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.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 public class InsteonDevice {
50 private final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
52 public enum DeviceStatus {
57 /** need to wait after query to avoid misinterpretation of duplicate replies */
58 private static final int QUIET_TIME_DIRECT_MESSAGE = 2000;
59 /** how far to space out poll messages */
60 private static final int TIME_BETWEEN_POLL_MESSAGES = 1500;
62 private InsteonAddress address = new InsteonAddress();
63 private long pollInterval = -1L; // in milliseconds
64 private @Nullable Driver driver = null;
65 private Map<String, DeviceFeature> features = new HashMap<>();
66 private @Nullable String productKey = null;
67 private volatile long lastTimePolled = 0L;
68 private volatile long lastMsgReceived = 0L;
69 private boolean isModem = false;
70 private PriorityQueue<@Nullable QEntry> mrequestQueue = new PriorityQueue<>();
71 private @Nullable DeviceFeature featureQueried = null;
72 private long lastQueryTime = 0L;
73 private boolean hasModemDBEntry = false;
74 private DeviceStatus status = DeviceStatus.INITIALIZED;
75 private Map<Integer, GroupMessageStateMachine> groupState = new HashMap<>();
76 private Map<String, Object> deviceConfigMap = new HashMap<>();
81 public InsteonDevice() {
82 lastMsgReceived = System.currentTimeMillis();
85 // --------------------- simple getters -----------------------------
87 public boolean hasProductKey() {
88 return productKey != null;
91 public @Nullable String getProductKey() {
95 public boolean hasModemDBEntry() {
96 return hasModemDBEntry;
99 public DeviceStatus getStatus() {
103 public InsteonAddress getAddress() {
107 public @Nullable Driver getDriver() {
111 public long getPollInterval() {
115 public boolean isModem() {
119 public @Nullable DeviceFeature getFeature(String f) {
120 return features.get(f);
123 public Map<String, DeviceFeature> getFeatures() {
127 public byte getX10HouseCode() {
128 return (address.getX10HouseCode());
131 public byte getX10UnitCode() {
132 return (address.getX10UnitCode());
135 public boolean hasProductKey(String key) {
136 String productKey = this.productKey;
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, Object> deviceConfigMap) {
198 this.deviceConfigMap = deviceConfigMap;
201 public Map<String, 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, 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 = RequestQueueManager.instance();
279 if (instance != null) {
280 instance.addQueue(this, now + delay);
282 logger.warn("request queue manager is null");
286 lastTimePolled = now;
291 * Handle incoming message for this device by forwarding
292 * it to all features that this device supports
294 * @param msg the incoming message
296 public void handleMessage(Msg msg) {
297 lastMsgReceived = System.currentTimeMillis();
298 synchronized (features) {
299 // first update all features that are
300 // not status features
301 for (DeviceFeature f : features.values()) {
302 if (!f.isStatusFeature()) {
303 logger.debug("----- applying message to feature: {}", f.getName());
304 if (f.handleMessage(msg)) {
305 // handled a reply to a query,
306 // mark it as processed
307 logger.trace("handled reply of direct: {}", f);
308 setFeatureQueried(null);
313 // then update all the status features,
314 // e.g. when the device was last updated
315 for (DeviceFeature f : features.values()) {
316 if (f.isStatusFeature()) {
317 f.handleMessage(msg);
324 * Helper method to make standard message
329 * @return standard message
330 * @throws FieldException
331 * @throws InvalidMessageTypeException
333 public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2)
334 throws FieldException, InvalidMessageTypeException {
335 return (makeStandardMessage(flags, cmd1, cmd2, -1));
339 * Helper method to make standard message, possibly with group
344 * @param group (-1 if not a group message)
345 * @return standard message
346 * @throws FieldException
347 * @throws InvalidMessageTypeException
349 public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2, int group)
350 throws FieldException, InvalidMessageTypeException {
351 Msg m = Msg.makeMessage("SendStandardMessage");
352 InsteonAddress addr = null;
355 f |= 0xc0; // mark message as group message
356 // and stash the group number into the address
357 addr = new InsteonAddress((byte) 0, (byte) 0, (byte) (group & 0xff));
361 m.setAddress("toAddress", addr);
362 m.setByte("messageFlags", f);
363 m.setByte("command1", cmd1);
364 m.setByte("command2", cmd2);
368 public Msg makeX10Message(byte rawX10, byte X10Flag) throws FieldException, InvalidMessageTypeException {
369 Msg m = Msg.makeMessage("SendX10Message");
370 m.setByte("rawX10", rawX10);
371 m.setByte("X10Flag", X10Flag);
372 m.setQuietTime(300L);
377 * Helper method to make extended message
382 * @return extended message
383 * @throws FieldException
384 * @throws InvalidMessageTypeException
386 public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2)
387 throws FieldException, InvalidMessageTypeException {
388 return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
392 * Helper method to make extended message
397 * @param data array with userdata
398 * @return extended message
399 * @throws FieldException
400 * @throws InvalidMessageTypeException
402 public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2, byte[] data)
403 throws FieldException, InvalidMessageTypeException {
404 Msg m = Msg.makeMessage("SendExtendedMessage");
405 m.setAddress("toAddress", getAddress());
406 m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
407 m.setByte("command1", cmd1);
408 m.setByte("command2", cmd2);
415 * Helper method to make extended message, but with different CRC calculation
420 * @param data array with user data
421 * @return extended message
422 * @throws FieldException
423 * @throws InvalidMessageTypeException
425 public Msg makeExtendedMessageCRC2(byte flags, byte cmd1, byte cmd2, byte[] data)
426 throws FieldException, InvalidMessageTypeException {
427 Msg m = Msg.makeMessage("SendExtendedMessage");
428 m.setAddress("toAddress", getAddress());
429 m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
430 m.setByte("command1", cmd1);
431 m.setByte("command2", cmd2);
438 * Called by the RequestQueueManager when the queue has expired
441 * @return time when to schedule the next message (timeNow + quietTime)
443 public long processRequestQueue(long timeNow) {
444 synchronized (mrequestQueue) {
445 if (mrequestQueue.isEmpty()) {
448 DeviceFeature featureQueried = this.featureQueried;
449 if (featureQueried != null) {
450 // A feature has been queried, but
451 // the response has not been digested yet.
452 // Must wait for the query to be processed.
453 long dt = timeNow - (lastQueryTime + featureQueried.getDirectAckTimeout());
455 logger.debug("still waiting for query reply from {} for another {} usec", address, -dt);
456 return (timeNow + 2000L); // retry soon
458 logger.debug("gave up waiting for query reply from device {}", address);
461 QEntry qe = mrequestQueue.poll(); // take it off the queue!
465 if (!qe.getMsg().isBroadcast()) {
466 logger.debug("qe taken off direct: {} {}", qe.getFeature(), qe.getMsg());
467 lastQueryTime = timeNow;
468 // mark feature as pending
469 qe.getFeature().setQueryStatus(DeviceFeature.QueryStatus.QUERY_PENDING);
470 // also mark this queue as pending so there is no doubt
471 this.featureQueried = qe.getFeature();
473 logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
475 long quietTime = qe.getMsg().getQuietTime();
476 qe.getMsg().setQuietTime(500L); // rate limiting downstream!
478 writeMessage(qe.getMsg());
479 } catch (IOException e) {
480 logger.warn("message write failed for msg {}", qe.getMsg(), e);
482 // figure out when the request queue should be checked next
483 QEntry qnext = mrequestQueue.peek();
484 long nextExpTime = (qnext == null ? 0L : qnext.getExpirationTime());
485 long nextTime = Math.max(timeNow + quietTime, nextExpTime);
486 logger.debug("next request queue processed in {} msec, quiettime = {}", nextTime - timeNow, quietTime);
492 * Enqueues message to be sent at the next possible time
494 * @param m message to be sent
495 * @param f device feature that sent this message (so we can associate the response message with it)
497 public void enqueueMessage(Msg m, DeviceFeature f) {
498 enqueueDelayedMessage(m, f, 0);
502 * Enqueues message to be sent after a delay
504 * @param m message to be sent
505 * @param f device feature that sent this message (so we can associate the response message with it)
506 * @param delay time (in milliseconds) to delay before enqueuing message
508 public void enqueueDelayedMessage(Msg m, DeviceFeature f, long delay) {
509 long now = System.currentTimeMillis();
510 synchronized (mrequestQueue) {
511 mrequestQueue.add(new QEntry(f, m, now + delay));
513 if (!m.isBroadcast()) {
514 m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
516 logger.trace("enqueing direct message with delay {}", delay);
517 RequestQueueManager instance = RequestQueueManager.instance();
518 if (instance != null) {
519 instance.addQueue(this, now + delay);
521 logger.warn("request queue manger instance is null");
525 private void writeMessage(Msg m) throws IOException {
526 Driver driver = this.driver;
527 if (driver != null) {
528 driver.writeMessage(m);
532 private void instantiateFeatures(DeviceType dt) {
533 for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
534 DeviceFeature f = DeviceFeature.makeDeviceFeature(fe.getValue());
536 logger.warn("device type {} references unknown feature: {}", dt, fe.getValue());
538 addFeature(fe.getKey(), f);
541 for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
542 FeatureGroup fg = fe.getValue();
544 DeviceFeature f = DeviceFeature.makeDeviceFeature(fg.getType());
546 logger.warn("device type {} references unknown feature group: {}", dt, fg.getType());
548 addFeature(fe.getKey(), f);
549 connectFeatures(fe.getKey(), f, fg.getFeatures());
554 private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> fgFeatures) {
555 for (String fs : fgFeatures) {
557 DeviceFeature f = features.get(fs);
559 logger.warn("feature group {} references unknown feature {}", gn, fs);
561 logger.debug("{} connected feature: {}", gn, f);
562 fg.addConnectedFeature(f);
567 private void addFeature(String name, DeviceFeature f) {
569 synchronized (features) {
570 features.put(name, f);
575 * Get the state of the state machine that suppresses duplicates for group messages.
576 * The state machine is advance the first time it is called for a message,
577 * otherwise return the current state.
579 * @param group the insteon group of the broadcast message
580 * @param a the type of group message came in (action etc)
581 * @param cmd1 cmd1 from the message received
582 * @return true if this is message is NOT a duplicate
584 public boolean getGroupState(int group, GroupMessage a, byte cmd1) {
585 GroupMessageStateMachine m = groupState.get(group);
587 m = new GroupMessageStateMachine();
588 groupState.put(group, m);
589 logger.trace("{} created group {} state", address, group);
591 if (lastMsgReceived <= m.getLastUpdated()) {
592 logger.trace("{} using previous group {} state for {}", address, group, a);
593 return m.getPublish();
597 logger.trace("{} updating group {} state to {}", address, group, a);
598 return (m.action(a, address, group, cmd1));
602 public String toString() {
603 String s = address.toString();
604 for (Entry<String, DeviceFeature> f : features.entrySet()) {
605 s += "|" + f.getKey() + "->" + f.getValue().toString();
613 * @param dt device type after which to model the device
614 * @return newly created device
616 public static InsteonDevice makeDevice(DeviceType dt) {
617 InsteonDevice dev = new InsteonDevice();
618 dev.instantiateFeatures(dt);
623 * Queue entry helper class
625 * @author Bernd Pfrommer - Initial contribution
627 public static class QEntry implements Comparable<QEntry> {
628 private DeviceFeature feature;
630 private long expirationTime;
632 public DeviceFeature getFeature() {
636 public Msg getMsg() {
640 public long getExpirationTime() {
641 return expirationTime;
644 QEntry(DeviceFeature f, Msg m, long t) {
651 public int compareTo(QEntry a) {
652 return (int) (expirationTime - a.expirationTime);