]> git.basschouten.com Git - openhab-addons.git/blob
fa21489efeb476aeb9225d3110ed8cdaa3a81777
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.insteon.internal;
14
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;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ScheduledExecutorService;
28
29 import javax.xml.parsers.ParserConfigurationException;
30
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;
61
62 /**
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.
65  *
66  * -----------------------------------------------------------------------------------------------
67  *
68  * This class represents the actual implementation of the binding, and controls the high level flow
69  * of messages to and from the InsteonModem.
70  *
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:
75  *
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.
79  *
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.
86  *
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.
92  *
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.
97  *
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.
102  *
103  *
104  * @author Bernd Pfrommer - Initial contribution
105  * @author Daniel Pfrommer - openHAB 1 insteonplm binding
106  * @author Rob Nielsen - Port to openHAB 2 insteon binding
107  */
108 @NonNullByDefault
109 public class InsteonBinding {
110     private static final int DEAD_DEVICE_COUNT = 10;
111
112     private final Logger logger = LoggerFactory.getLogger(InsteonBinding.class);
113
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 int messagesReceived = 0;
121     private boolean isActive = false; // state of binding
122     private int x10HouseUnit = -1;
123     private InsteonNetworkHandler handler;
124
125     public InsteonBinding(InsteonNetworkHandler handler, InsteonNetworkConfiguration config,
126             SerialPortManager serialPortManager, ScheduledExecutorService scheduler) {
127         this.handler = handler;
128
129         String port = config.getPort();
130         logger.debug("port = '{}'", Utils.redactPassword(port));
131
132         driver = new Driver(port, portListener, serialPortManager, scheduler);
133         driver.addMsgListener(portListener);
134
135         Integer devicePollIntervalSeconds = config.getDevicePollIntervalSeconds();
136         if (devicePollIntervalSeconds != null) {
137             devicePollIntervalMilliseconds = devicePollIntervalSeconds * 1000;
138         }
139         logger.debug("device poll interval set to {} seconds", devicePollIntervalMilliseconds / 1000);
140
141         String additionalDevices = config.getAdditionalDevices();
142         if (additionalDevices != null) {
143             try {
144                 DeviceTypeLoader instance = DeviceTypeLoader.instance();
145                 if (instance != null) {
146                     instance.loadDeviceTypesXML(additionalDevices);
147                     logger.debug("read additional device definitions from {}", additionalDevices);
148                 } else {
149                     logger.warn("device type loader instance is null");
150                 }
151             } catch (ParserConfigurationException | SAXException | IOException e) {
152                 logger.warn("error reading additional devices from {}", additionalDevices, e);
153             }
154         }
155
156         String additionalFeatures = config.getAdditionalFeatures();
157         if (additionalFeatures != null) {
158             logger.debug("reading additional feature templates from {}", additionalFeatures);
159             DeviceFeature.readFeatureTemplates(additionalFeatures);
160         }
161
162         deadDeviceTimeout = devicePollIntervalMilliseconds * DEAD_DEVICE_COUNT;
163         logger.debug("dead device timeout set to {} seconds", deadDeviceTimeout / 1000);
164     }
165
166     public Driver getDriver() {
167         return driver;
168     }
169
170     public boolean startPolling() {
171         logger.debug("starting to poll {}", driver.getPortName());
172         driver.start();
173         return driver.isRunning();
174     }
175
176     public void setIsActive(boolean isActive) {
177         this.isActive = isActive;
178     }
179
180     public void sendCommand(String channelName, Command command) {
181         if (!isActive) {
182             logger.debug("not ready to handle commands yet, returning.");
183             return;
184         }
185
186         InsteonChannelConfiguration bindingConfig = bindingConfigs.get(channelName);
187         if (bindingConfig == null) {
188             logger.warn("unable to find binding config for channel {}", channelName);
189             return;
190         }
191
192         InsteonDevice dev = getDevice(bindingConfig.getAddress());
193         if (dev == null) {
194             logger.warn("no device found with insteon address {}", bindingConfig.getAddress());
195             return;
196         }
197
198         dev.processCommand(driver, bindingConfig, command);
199
200         logger.debug("found binding config for channel {}", channelName);
201     }
202
203     public void addFeatureListener(InsteonChannelConfiguration bindingConfig) {
204         logger.debug("adding listener for channel {}", bindingConfig.getChannelName());
205
206         InsteonAddress address = bindingConfig.getAddress();
207         InsteonDevice dev = getDevice(address);
208         if (dev == null) {
209             logger.warn("device for address {} is null", address);
210             return;
211         }
212         @Nullable
213         DeviceFeature f = dev.getFeature(bindingConfig.getFeature());
214         if (f == null || f.isFeatureGroup()) {
215             StringBuilder buf = new StringBuilder();
216             ArrayList<String> names = new ArrayList<>(dev.getFeatures().keySet());
217             Collections.sort(names);
218             for (String name : names) {
219                 DeviceFeature feature = dev.getFeature(name);
220                 if (feature != null && !feature.isFeatureGroup()) {
221                     if (buf.length() > 0) {
222                         buf.append(", ");
223                     }
224                     buf.append(name);
225                 }
226             }
227
228             logger.warn("channel {} references unknown feature: {}, it will be ignored. Known features for {} are: {}.",
229                     bindingConfig.getChannelName(), bindingConfig.getFeature(), bindingConfig.getProductKey(),
230                     buf.toString());
231             return;
232         }
233
234         DeviceFeatureListener fl = new DeviceFeatureListener(this, bindingConfig.getChannelUID(),
235                 bindingConfig.getChannelName());
236         fl.setParameters(bindingConfig.getParameters());
237         f.addListener(fl);
238
239         bindingConfigs.put(bindingConfig.getChannelName(), bindingConfig);
240     }
241
242     public void removeFeatureListener(ChannelUID channelUID) {
243         String channelName = channelUID.getAsString();
244
245         logger.debug("removing listener for channel {}", channelName);
246
247         for (Iterator<Entry<InsteonAddress, InsteonDevice>> it = devices.entrySet().iterator(); it.hasNext();) {
248             InsteonDevice dev = it.next().getValue();
249             boolean removedListener = dev.removeFeatureListener(channelName);
250             if (removedListener) {
251                 logger.trace("removed feature listener {} from dev {}", channelName, dev);
252             }
253         }
254     }
255
256     public void updateFeatureState(ChannelUID channelUID, State state) {
257         handler.updateState(channelUID, state);
258     }
259
260     public @Nullable InsteonDevice makeNewDevice(InsteonAddress addr, String productKey,
261             Map<String, Object> deviceConfigMap) {
262         DeviceTypeLoader instance = DeviceTypeLoader.instance();
263         if (instance == null) {
264             return null;
265         }
266         DeviceType dt = instance.getDeviceType(productKey);
267         if (dt == null) {
268             return null;
269         }
270         InsteonDevice dev = InsteonDevice.makeDevice(dt);
271         dev.setAddress(addr);
272         dev.setProductKey(productKey);
273         dev.setDriver(driver);
274         dev.setIsModem(productKey.equals(InsteonDeviceHandler.PLM_PRODUCT_KEY));
275         dev.setDeviceConfigMap(deviceConfigMap);
276         if (!dev.hasValidPollingInterval()) {
277             dev.setPollInterval(devicePollIntervalMilliseconds);
278         }
279         if (driver.isModemDBComplete() && dev.getStatus() != DeviceStatus.POLLING) {
280             int ndev = checkIfInModemDatabase(dev);
281             if (dev.hasModemDBEntry()) {
282                 dev.setStatus(DeviceStatus.POLLING);
283                 Poller.instance().startPolling(dev, ndev);
284             }
285         }
286         devices.put(addr, dev);
287
288         handler.insteonDeviceWasCreated();
289
290         return (dev);
291     }
292
293     public void removeDevice(InsteonAddress addr) {
294         InsteonDevice dev = devices.remove(addr);
295         if (dev == null) {
296             return;
297         }
298
299         if (dev.getStatus() == DeviceStatus.POLLING) {
300             Poller.instance().stopPolling(dev);
301         }
302     }
303
304     /**
305      * Checks if a device is in the modem link database, and, if the database
306      * is complete, logs a warning if the device is not present
307      *
308      * @param dev The device to search for in the modem database
309      * @return number of devices in modem database
310      */
311     private int checkIfInModemDatabase(InsteonDevice dev) {
312         try {
313             InsteonAddress addr = dev.getAddress();
314             Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
315             if (dbes.containsKey(addr)) {
316                 if (!dev.hasModemDBEntry()) {
317                     logger.debug("device {} found in the modem database and {}.", addr, getLinkInfo(dbes, addr, true));
318                     dev.setHasModemDBEntry(true);
319                 }
320             } else {
321                 if (driver.isModemDBComplete() && !addr.isX10()) {
322                     logger.warn("device {} not found in the modem database. Did you forget to link?", addr);
323                     handler.deviceNotLinked(addr);
324                 }
325             }
326             return dbes.size();
327         } finally {
328             driver.unlockModemDBEntries();
329         }
330     }
331
332     public Map<String, String> getDatabaseInfo() {
333         try {
334             Map<String, String> databaseInfo = new HashMap<>();
335             Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
336             for (InsteonAddress addr : dbes.keySet()) {
337                 String a = addr.toString();
338                 databaseInfo.put(a, a + ": " + getLinkInfo(dbes, addr, false));
339             }
340
341             return databaseInfo;
342         } finally {
343             driver.unlockModemDBEntries();
344         }
345     }
346
347     public boolean reconnect() {
348         driver.stop();
349         return startPolling();
350     }
351
352     /**
353      * Everything below was copied from Insteon PLM v1
354      */
355
356     /**
357      * Clean up all state.
358      */
359     public void shutdown() {
360         logger.debug("shutting down Insteon bridge");
361         driver.stop();
362         devices.clear();
363         RequestQueueManager.destroyInstance();
364         Poller.instance().stop();
365         isActive = false;
366     }
367
368     /**
369      * Method to find a device by address
370      *
371      * @param aAddr the insteon address to search for
372      * @return reference to the device, or null if not found
373      */
374     public @Nullable InsteonDevice getDevice(@Nullable InsteonAddress aAddr) {
375         InsteonDevice dev = (aAddr == null) ? null : devices.get(aAddr);
376         return (dev);
377     }
378
379     private String getLinkInfo(Map<InsteonAddress, ModemDBEntry> dbes, InsteonAddress a, boolean prefix) {
380         ModemDBEntry dbe = dbes.get(a);
381         if (dbe == null) {
382             return "";
383         }
384         List<Byte> controls = dbe.getControls();
385         List<Byte> responds = dbe.getRespondsTo();
386
387         Port port = dbe.getPort();
388         if (port == null) {
389             return "";
390         }
391         String deviceName = port.getDeviceName();
392         String s = deviceName.startsWith("/hub") ? "hub" : "plm";
393         StringBuilder buf = new StringBuilder();
394         if (port.isModem(a)) {
395             if (prefix) {
396                 buf.append("it is the ");
397             }
398             buf.append(s);
399             buf.append(" (");
400             buf.append(Utils.redactPassword(deviceName));
401             buf.append(")");
402         } else {
403             if (prefix) {
404                 buf.append("the ");
405             }
406             buf.append(s);
407             buf.append(" controls groups (");
408             buf.append(toGroupString(controls));
409             buf.append(") and responds to groups (");
410             buf.append(toGroupString(responds));
411             buf.append(")");
412         }
413
414         return buf.toString();
415     }
416
417     private String toGroupString(List<Byte> group) {
418         List<Byte> sorted = new ArrayList<>(group);
419         Collections.sort(sorted, new Comparator<Byte>() {
420             @Override
421             public int compare(Byte b1, Byte b2) {
422                 int i1 = b1 & 0xFF;
423                 int i2 = b2 & 0xFF;
424                 return i1 < i2 ? -1 : i1 == i2 ? 0 : 1;
425             }
426         });
427
428         StringBuilder buf = new StringBuilder();
429         for (Byte b : sorted) {
430             if (buf.length() > 0) {
431                 buf.append(",");
432             }
433             buf.append(b & 0xFF);
434         }
435
436         return buf.toString();
437     }
438
439     public void logDeviceStatistics() {
440         String msg = String.format("devices: %3d configured, %3d polling, msgs received: %5d", devices.size(),
441                 Poller.instance().getSizeOfQueue(), messagesReceived);
442         logger.debug("{}", msg);
443         messagesReceived = 0;
444         for (InsteonDevice dev : devices.values()) {
445             if (dev.isModem()) {
446                 continue;
447             }
448             if (deadDeviceTimeout > 0 && dev.getPollOverDueTime() > deadDeviceTimeout) {
449                 logger.debug("device {} has not responded to polls for {} sec", dev.toString(),
450                         dev.getPollOverDueTime() / 3600);
451             }
452         }
453     }
454
455     /**
456      * Handles messages that come in from the ports.
457      * Will only process one message at a time.
458      */
459     private class PortListener implements MsgListener, DriverListener {
460         @Override
461         public void msg(Msg msg) {
462             if (msg.isEcho() || msg.isPureNack()) {
463                 return;
464             }
465             messagesReceived++;
466             logger.debug("got msg: {}", msg);
467             if (msg.isX10()) {
468                 handleX10Message(msg);
469             } else {
470                 handleInsteonMessage(msg);
471             }
472         }
473
474         @Override
475         public void driverCompletelyInitialized() {
476             List<String> missing = new ArrayList<>();
477             try {
478                 Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
479                 logger.debug("modem database has {} entries!", dbes.size());
480                 if (dbes.isEmpty()) {
481                     logger.warn("the modem link database is empty!");
482                 }
483                 for (InsteonAddress k : dbes.keySet()) {
484                     logger.debug("modem db entry: {}", k);
485                 }
486                 Set<InsteonAddress> addrs = new HashSet<>();
487                 for (InsteonDevice dev : devices.values()) {
488                     InsteonAddress a = dev.getAddress();
489                     if (!dbes.containsKey(a)) {
490                         if (!a.isX10()) {
491                             logger.warn("device {} not found in the modem database. Did you forget to link?", a);
492                             handler.deviceNotLinked(a);
493                         }
494                     } else {
495                         if (!dev.hasModemDBEntry()) {
496                             addrs.add(a);
497                             logger.debug("device {} found in the modem database and {}.", a,
498                                     getLinkInfo(dbes, a, true));
499                             dev.setHasModemDBEntry(true);
500                         }
501                         if (dev.getStatus() != DeviceStatus.POLLING) {
502                             Poller.instance().startPolling(dev, dbes.size());
503                         }
504                     }
505                 }
506
507                 for (InsteonAddress k : dbes.keySet()) {
508                     if (!addrs.contains(k)) {
509                         logger.debug("device {} found in the modem database, but is not configured as a thing and {}.",
510                                 k, getLinkInfo(dbes, k, true));
511
512                         missing.add(k.toString());
513                     }
514                 }
515             } finally {
516                 driver.unlockModemDBEntries();
517             }
518
519             if (!missing.isEmpty()) {
520                 handler.addMissingDevices(missing);
521             }
522         }
523
524         @Override
525         public void disconnected() {
526             handler.bindingDisconnected();
527         }
528
529         private void handleInsteonMessage(Msg msg) {
530             InsteonAddress toAddr = msg.getAddr("toAddress");
531             if (!msg.isBroadcast() && !driver.isMsgForUs(toAddr)) {
532                 // not for one of our modems, do not process
533                 return;
534             }
535             InsteonAddress fromAddr = msg.getAddr("fromAddress");
536             if (fromAddr == null) {
537                 logger.debug("invalid fromAddress, ignoring msg {}", msg);
538                 return;
539             }
540             handleMessage(fromAddr, msg);
541         }
542
543         private void handleX10Message(Msg msg) {
544             try {
545                 int x10Flag = msg.getByte("X10Flag") & 0xff;
546                 int rawX10 = msg.getByte("rawX10") & 0xff;
547                 if (x10Flag == 0x80) { // actual command
548                     if (x10HouseUnit != -1) {
549                         InsteonAddress fromAddr = new InsteonAddress((byte) x10HouseUnit);
550                         handleMessage(fromAddr, msg);
551                     }
552                 } else if (x10Flag == 0) {
553                     // what unit the next cmd will apply to
554                     x10HouseUnit = rawX10 & 0xFF;
555                 }
556             } catch (FieldException e) {
557                 logger.warn("got bad X10 message: {}", msg, e);
558                 return;
559             }
560         }
561
562         private void handleMessage(InsteonAddress fromAddr, Msg msg) {
563             InsteonDevice dev = getDevice(fromAddr);
564             if (dev == null) {
565                 logger.debug("dropping message from unknown device with address {}", fromAddr);
566             } else {
567                 dev.handleMessage(msg);
568             }
569         }
570     }
571 }