2 * Copyright (c) 2010-2020 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.device;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.List;
21 import java.util.Map.Entry;
22 import java.util.PriorityQueue;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
27 import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup;
28 import org.openhab.binding.insteon.internal.device.GroupMessageStateMachine.GroupMessage;
29 import org.openhab.binding.insteon.internal.driver.Driver;
30 import org.openhab.binding.insteon.internal.message.FieldException;
31 import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
32 import org.openhab.binding.insteon.internal.message.Msg;
33 import org.openhab.core.types.Command;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The InsteonDevice class holds known per-device state of a single Insteon device,
39 * including the address, what port(modem) to reach it on etc.
40 * Note that some Insteon devices de facto consist of two devices (let's say
41 * a relay and a sensor), but operate under the same address. Such devices will
42 * be represented just by a single InsteonDevice. Their different personalities
43 * will then be represented by DeviceFeatures.
45 * @author Bernd Pfrommer - Initial contribution
46 * @author Rob Nielsen - Port to openHAB 2 insteon binding
49 @SuppressWarnings("null")
50 public class InsteonDevice {
51 private final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
53 public static enum DeviceStatus {
58 /** need to wait after query to avoid misinterpretation of duplicate replies */
59 private static final int QUIET_TIME_DIRECT_MESSAGE = 2000;
60 /** how far to space out poll messages */
61 private static final int TIME_BETWEEN_POLL_MESSAGES = 1500;
63 private InsteonAddress address = new InsteonAddress();
64 private long pollInterval = -1L; // in milliseconds
65 private @Nullable Driver driver = null;
66 private HashMap<String, @Nullable DeviceFeature> features = new HashMap<>();
67 private @Nullable String productKey = null;
68 private volatile long lastTimePolled = 0L;
69 private volatile long lastMsgReceived = 0L;
70 private boolean isModem = false;
71 private PriorityQueue<@Nullable QEntry> mrequestQueue = new PriorityQueue<>();
72 private @Nullable DeviceFeature featureQueried = null;
73 private long lastQueryTime = 0L;
74 private boolean hasModemDBEntry = false;
75 private DeviceStatus status = DeviceStatus.INITIALIZED;
76 private Map<Integer, @Nullable GroupMessageStateMachine> groupState = new HashMap<>();
77 private Map<String, @Nullable Object> deviceConfigMap = new HashMap<String, @Nullable Object>();
82 public InsteonDevice() {
83 lastMsgReceived = System.currentTimeMillis();
86 // --------------------- simple getters -----------------------------
88 public boolean hasProductKey() {
89 return productKey != null;
92 public @Nullable String getProductKey() {
96 public boolean hasModemDBEntry() {
97 return hasModemDBEntry;
100 public DeviceStatus getStatus() {
104 public InsteonAddress getAddress() {
108 public @Nullable Driver getDriver() {
112 public long getPollInterval() {
116 public boolean isModem() {
120 public @Nullable DeviceFeature getFeature(String f) {
121 return features.get(f);
124 public HashMap<String, @Nullable DeviceFeature> getFeatures() {
128 public byte getX10HouseCode() {
129 return (address.getX10HouseCode());
132 public byte getX10UnitCode() {
133 return (address.getX10UnitCode());
136 public boolean hasProductKey(String key) {
137 return productKey != null && productKey.equals(key);
140 public boolean hasValidPollingInterval() {
141 return (pollInterval > 0);
144 public long getPollOverDueTime() {
145 return (lastTimePolled - lastMsgReceived);
148 public boolean hasAnyListeners() {
149 synchronized (features) {
150 for (DeviceFeature f : features.values()) {
151 if (f.hasListeners()) {
158 // --------------------- simple setters -----------------------------
160 public void setStatus(DeviceStatus aI) {
164 public void setHasModemDBEntry(boolean b) {
168 public void setAddress(InsteonAddress ia) {
172 public void setDriver(Driver d) {
176 public void setIsModem(boolean f) {
180 public void setProductKey(String pk) {
184 public void setPollInterval(long pi) {
185 logger.trace("setting poll interval for {} to {} ", address, pi);
191 public void setFeatureQueried(@Nullable DeviceFeature f) {
192 synchronized (mrequestQueue) {
197 public void setDeviceConfigMap(Map<String, @Nullable Object> deviceConfigMap) {
198 this.deviceConfigMap = deviceConfigMap;
201 public Map<String, @Nullable Object> getDeviceConfigMap() {
202 return deviceConfigMap;
205 public @Nullable DeviceFeature getFeatureQueried() {
206 synchronized (mrequestQueue) {
207 return (featureQueried);
212 * Removes feature listener from this device
214 * @param aItemName name of the feature listener to remove
215 * @return true if a feature listener was successfully removed
217 public boolean removeFeatureListener(String aItemName) {
218 boolean removedListener = false;
219 synchronized (features) {
220 for (Iterator<Entry<String, @Nullable DeviceFeature>> it = features.entrySet().iterator(); it.hasNext();) {
221 DeviceFeature f = it.next().getValue();
222 if (f.removeListener(aItemName)) {
223 removedListener = true;
227 return removedListener;
231 * Invoked to process an openHAB command
233 * @param driver The driver to use
234 * @param c The item configuration
235 * @param command The actual command to execute
237 public void processCommand(Driver driver, InsteonChannelConfiguration c, Command command) {
238 logger.debug("processing command {} features: {}", command, features.size());
239 synchronized (features) {
240 for (DeviceFeature i : features.values()) {
241 if (i.isReferencedByItem(c.getChannelName())) {
242 i.handleCommand(c, command);
249 * Execute poll on this device: create an array of messages,
250 * add them to the request queue, and schedule the queue
253 * @param delay scheduling delay (in milliseconds)
255 public void doPoll(long delay) {
256 long now = System.currentTimeMillis();
257 List<QEntry> l = new ArrayList<>();
258 synchronized (features) {
260 for (DeviceFeature i : features.values()) {
261 if (i.hasListeners()) {
262 Msg m = i.makePollMsg();
264 l.add(new QEntry(i, m, now + delay + spacing));
265 spacing += TIME_BETWEEN_POLL_MESSAGES;
273 synchronized (mrequestQueue) {
275 mrequestQueue.add(e);
278 RequestQueueManager.instance().addQueue(this, now + delay);
281 lastTimePolled = now;
286 * Handle incoming message for this device by forwarding
287 * it to all features that this device supports
289 * @param msg the incoming message
291 public void handleMessage(Msg msg) {
292 lastMsgReceived = System.currentTimeMillis();
293 synchronized (features) {
294 // first update all features that are
295 // not status features
296 for (DeviceFeature f : features.values()) {
297 if (!f.isStatusFeature()) {
298 logger.debug("----- applying message to feature: {}", f.getName());
299 if (f.handleMessage(msg)) {
300 // handled a reply to a query,
301 // mark it as processed
302 logger.trace("handled reply of direct: {}", f);
303 setFeatureQueried(null);
308 // then update all the status features,
309 // e.g. when the device was last updated
310 for (DeviceFeature f : features.values()) {
311 if (f.isStatusFeature()) {
312 f.handleMessage(msg);
319 * Helper method to make standard message
324 * @return standard message
325 * @throws FieldException
326 * @throws IOException
328 public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2)
329 throws FieldException, InvalidMessageTypeException {
330 return (makeStandardMessage(flags, cmd1, cmd2, -1));
334 * Helper method to make standard message, possibly with group
339 * @param group (-1 if not a group message)
340 * @return standard message
341 * @throws FieldException
342 * @throws IOException
344 public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2, int group)
345 throws FieldException, InvalidMessageTypeException {
346 Msg m = Msg.makeMessage("SendStandardMessage");
347 InsteonAddress addr = null;
350 f |= 0xc0; // mark message as group message
351 // and stash the group number into the address
352 addr = new InsteonAddress((byte) 0, (byte) 0, (byte) (group & 0xff));
356 m.setAddress("toAddress", addr);
357 m.setByte("messageFlags", f);
358 m.setByte("command1", cmd1);
359 m.setByte("command2", cmd2);
363 public Msg makeX10Message(byte rawX10, byte X10Flag) throws FieldException, InvalidMessageTypeException {
364 Msg m = Msg.makeMessage("SendX10Message");
365 m.setByte("rawX10", rawX10);
366 m.setByte("X10Flag", X10Flag);
367 m.setQuietTime(300L);
372 * Helper method to make extended message
377 * @return extended message
378 * @throws FieldException
379 * @throws IOException
381 public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2)
382 throws FieldException, InvalidMessageTypeException {
383 return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
387 * Helper method to make extended message
392 * @param data array with userdata
393 * @return extended message
394 * @throws FieldException
395 * @throws IOException
397 public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2, byte[] data)
398 throws FieldException, InvalidMessageTypeException {
399 Msg m = Msg.makeMessage("SendExtendedMessage");
400 m.setAddress("toAddress", getAddress());
401 m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
402 m.setByte("command1", cmd1);
403 m.setByte("command2", cmd2);
410 * Helper method to make extended message, but with different CRC calculation
415 * @param data array with user data
416 * @return extended message
417 * @throws FieldException
418 * @throws IOException
420 public Msg makeExtendedMessageCRC2(byte flags, byte cmd1, byte cmd2, byte[] data)
421 throws FieldException, InvalidMessageTypeException {
422 Msg m = Msg.makeMessage("SendExtendedMessage");
423 m.setAddress("toAddress", getAddress());
424 m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
425 m.setByte("command1", cmd1);
426 m.setByte("command2", cmd2);
433 * Called by the RequestQueueManager when the queue has expired
436 * @return time when to schedule the next message (timeNow + quietTime)
438 public long processRequestQueue(long timeNow) {
439 synchronized (mrequestQueue) {
440 if (mrequestQueue.isEmpty()) {
443 if (featureQueried != null) {
444 // A feature has been queried, but
445 // the response has not been digested yet.
446 // Must wait for the query to be processed.
447 long dt = timeNow - (lastQueryTime + featureQueried.getDirectAckTimeout());
449 logger.debug("still waiting for query reply from {} for another {} usec", address, -dt);
450 return (timeNow + 2000L); // retry soon
452 logger.debug("gave up waiting for query reply from device {}", address);
455 QEntry qe = mrequestQueue.poll(); // take it off the queue!
456 if (!qe.getMsg().isBroadcast()) {
457 logger.debug("qe taken off direct: {} {}", qe.getFeature(), qe.getMsg());
458 lastQueryTime = timeNow;
459 // mark feature as pending
460 qe.getFeature().setQueryStatus(DeviceFeature.QueryStatus.QUERY_PENDING);
461 // also mark this queue as pending so there is no doubt
462 featureQueried = qe.getFeature();
464 logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
466 long quietTime = qe.getMsg().getQuietTime();
467 qe.getMsg().setQuietTime(500L); // rate limiting downstream!
469 writeMessage(qe.getMsg());
470 } catch (IOException e) {
471 logger.warn("message write failed for msg {}", qe.getMsg(), e);
473 // figure out when the request queue should be checked next
474 QEntry qnext = mrequestQueue.peek();
475 long nextExpTime = (qnext == null ? 0L : qnext.getExpirationTime());
476 long nextTime = Math.max(timeNow + quietTime, nextExpTime);
477 logger.debug("next request queue processed in {} msec, quiettime = {}", nextTime - timeNow, quietTime);
483 * Enqueues message to be sent at the next possible time
485 * @param m message to be sent
486 * @param f device feature that sent this message (so we can associate the response message with it)
488 public void enqueueMessage(Msg m, DeviceFeature f) {
489 enqueueDelayedMessage(m, f, 0);
493 * Enqueues message to be sent after a delay
495 * @param m message to be sent
496 * @param f device feature that sent this message (so we can associate the response message with it)
497 * @param d time (in milliseconds)to delay before enqueuing message
499 public void enqueueDelayedMessage(Msg m, DeviceFeature f, long delay) {
500 long now = System.currentTimeMillis();
501 synchronized (mrequestQueue) {
502 mrequestQueue.add(new QEntry(f, m, now + delay));
504 if (!m.isBroadcast()) {
505 m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
507 logger.trace("enqueing direct message with delay {}", delay);
508 RequestQueueManager.instance().addQueue(this, now + delay);
511 private void writeMessage(Msg m) throws IOException {
512 driver.writeMessage(m);
515 private void instantiateFeatures(@Nullable DeviceType dt) {
516 for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
517 DeviceFeature f = DeviceFeature.makeDeviceFeature(fe.getValue());
519 logger.warn("device type {} references unknown feature: {}", dt, fe.getValue());
521 addFeature(fe.getKey(), f);
524 for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
525 FeatureGroup fg = fe.getValue();
527 DeviceFeature f = DeviceFeature.makeDeviceFeature(fg.getType());
529 logger.warn("device type {} references unknown feature group: {}", dt, fg.getType());
531 addFeature(fe.getKey(), f);
532 connectFeatures(fe.getKey(), f, fg.getFeatures());
537 private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> fgFeatures) {
538 for (String fs : fgFeatures) {
540 DeviceFeature f = features.get(fs);
542 logger.warn("feature group {} references unknown feature {}", gn, fs);
544 logger.debug("{} connected feature: {}", gn, f);
545 fg.addConnectedFeature(f);
550 private void addFeature(String name, DeviceFeature f) {
552 synchronized (features) {
553 features.put(name, f);
558 * Get the state of the state machine that suppresses duplicates for group messages.
559 * The state machine is advance the first time it is called for a message,
560 * otherwise return the current state.
562 * @param group the insteon group of the broadcast message
563 * @param a the type of group message came in (action etc)
564 * @param cmd1 cmd1 from the message received
565 * @return true if this is message is NOT a duplicate
567 public boolean getGroupState(int group, GroupMessage a, byte cmd1) {
568 GroupMessageStateMachine m = groupState.get(group);
570 m = new GroupMessageStateMachine();
571 groupState.put(group, m);
572 logger.trace("{} created group {} state", address, group);
574 if (lastMsgReceived <= m.getLastUpdated()) {
575 logger.trace("{} using previous group {} state for {}", address, group, a);
576 return m.getPublish();
580 logger.trace("{} updating group {} state to {}", address, group, a);
581 return (m.action(a, address, group, cmd1));
585 public String toString() {
586 String s = address.toString();
587 for (Entry<String, @Nullable DeviceFeature> f : features.entrySet()) {
588 s += "|" + f.getKey() + "->" + f.getValue().toString();
596 * @param dt device type after which to model the device
597 * @return newly created device
599 public static InsteonDevice makeDevice(@Nullable DeviceType dt) {
600 InsteonDevice dev = new InsteonDevice();
601 dev.instantiateFeatures(dt);
606 * Queue entry helper class
608 * @author Bernd Pfrommer - Initial contribution
611 public static class QEntry implements Comparable<QEntry> {
612 private DeviceFeature feature;
614 private long expirationTime;
616 public DeviceFeature getFeature() {
620 public Msg getMsg() {
624 public long getExpirationTime() {
625 return expirationTime;
628 QEntry(DeviceFeature f, Msg m, long t) {
635 public int compareTo(QEntry a) {
636 return (int) (expirationTime - a.expirationTime);