]> git.basschouten.com Git - openhab-addons.git/blob
db4430f3f8448c2db214c51be0d8d326ac097689
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.device;
14
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;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.PriorityQueue;
23
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;
36
37 /**
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.
44  *
45  * @author Bernd Pfrommer - Initial contribution
46  * @author Rob Nielsen - Port to openHAB 2 insteon binding
47  */
48 @NonNullByDefault
49 public class InsteonDevice {
50     private final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
51
52     public enum DeviceStatus {
53         INITIALIZED,
54         POLLING
55     }
56
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;
61
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<>();
77
78     /**
79      * Constructor
80      */
81     public InsteonDevice() {
82         lastMsgReceived = System.currentTimeMillis();
83     }
84
85     // --------------------- simple getters -----------------------------
86
87     public boolean hasProductKey() {
88         return productKey != null;
89     }
90
91     public @Nullable String getProductKey() {
92         return productKey;
93     }
94
95     public boolean hasModemDBEntry() {
96         return hasModemDBEntry;
97     }
98
99     public DeviceStatus getStatus() {
100         return status;
101     }
102
103     public InsteonAddress getAddress() {
104         return (address);
105     }
106
107     public @Nullable Driver getDriver() {
108         return driver;
109     }
110
111     public long getPollInterval() {
112         return pollInterval;
113     }
114
115     public boolean isModem() {
116         return isModem;
117     }
118
119     public @Nullable DeviceFeature getFeature(String f) {
120         return features.get(f);
121     }
122
123     public Map<String, DeviceFeature> getFeatures() {
124         return features;
125     }
126
127     public byte getX10HouseCode() {
128         return (address.getX10HouseCode());
129     }
130
131     public byte getX10UnitCode() {
132         return (address.getX10UnitCode());
133     }
134
135     public boolean hasProductKey(String key) {
136         String productKey = this.productKey;
137         return productKey != null && productKey.equals(key);
138     }
139
140     public boolean hasValidPollingInterval() {
141         return (pollInterval > 0);
142     }
143
144     public long getPollOverDueTime() {
145         return (lastTimePolled - lastMsgReceived);
146     }
147
148     public boolean hasAnyListeners() {
149         synchronized (features) {
150             for (DeviceFeature f : features.values()) {
151                 if (f.hasListeners()) {
152                     return true;
153                 }
154             }
155         }
156         return false;
157     }
158     // --------------------- simple setters -----------------------------
159
160     public void setStatus(DeviceStatus aI) {
161         status = aI;
162     }
163
164     public void setHasModemDBEntry(boolean b) {
165         hasModemDBEntry = b;
166     }
167
168     public void setAddress(InsteonAddress ia) {
169         address = ia;
170     }
171
172     public void setDriver(Driver d) {
173         driver = d;
174     }
175
176     public void setIsModem(boolean f) {
177         isModem = f;
178     }
179
180     public void setProductKey(String pk) {
181         productKey = pk;
182     }
183
184     public void setPollInterval(long pi) {
185         logger.trace("setting poll interval for {} to {} ", address, pi);
186         if (pi > 0) {
187             pollInterval = pi;
188         }
189     }
190
191     public void setFeatureQueried(@Nullable DeviceFeature f) {
192         synchronized (mrequestQueue) {
193             featureQueried = f;
194         }
195     }
196
197     public void setDeviceConfigMap(Map<String, Object> deviceConfigMap) {
198         this.deviceConfigMap = deviceConfigMap;
199     }
200
201     public Map<String, Object> getDeviceConfigMap() {
202         return deviceConfigMap;
203     }
204
205     public @Nullable DeviceFeature getFeatureQueried() {
206         synchronized (mrequestQueue) {
207             return (featureQueried);
208         }
209     }
210
211     /**
212      * Removes feature listener from this device
213      *
214      * @param aItemName name of the feature listener to remove
215      * @return true if a feature listener was successfully removed
216      */
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;
224                 }
225             }
226         }
227         return removedListener;
228     }
229
230     /**
231      * Invoked to process an openHAB command
232      *
233      * @param driver The driver to use
234      * @param c The item configuration
235      * @param command The actual command to execute
236      */
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);
243                 }
244             }
245         }
246     }
247
248     /**
249      * Execute poll on this device: create an array of messages,
250      * add them to the request queue, and schedule the queue
251      * for processing.
252      *
253      * @param delay scheduling delay (in milliseconds)
254      */
255     public void doPoll(long delay) {
256         long now = System.currentTimeMillis();
257         List<QEntry> l = new ArrayList<>();
258         synchronized (features) {
259             int spacing = 0;
260             for (DeviceFeature i : features.values()) {
261                 if (i.hasListeners()) {
262                     Msg m = i.makePollMsg();
263                     if (m != null) {
264                         l.add(new QEntry(i, m, now + delay + spacing));
265                         spacing += TIME_BETWEEN_POLL_MESSAGES;
266                     }
267                 }
268             }
269         }
270         if (l.isEmpty()) {
271             return;
272         }
273         synchronized (mrequestQueue) {
274             for (QEntry e : l) {
275                 mrequestQueue.add(e);
276             }
277         }
278         RequestQueueManager instance = RequestQueueManager.instance();
279         if (instance != null) {
280             instance.addQueue(this, now + delay);
281         } else {
282             logger.warn("request queue manager is null");
283         }
284
285         if (!l.isEmpty()) {
286             lastTimePolled = now;
287         }
288     }
289
290     /**
291      * Handle incoming message for this device by forwarding
292      * it to all features that this device supports
293      *
294      * @param msg the incoming message
295      */
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);
309                         break;
310                     }
311                 }
312             }
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);
318                 }
319             }
320         }
321     }
322
323     /**
324      * Helper method to make standard message
325      *
326      * @param flags
327      * @param cmd1
328      * @param cmd2
329      * @return standard message
330      * @throws FieldException
331      * @throws InvalidMessageTypeException
332      */
333     public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2)
334             throws FieldException, InvalidMessageTypeException {
335         return (makeStandardMessage(flags, cmd1, cmd2, -1));
336     }
337
338     /**
339      * Helper method to make standard message, possibly with group
340      *
341      * @param flags
342      * @param cmd1
343      * @param cmd2
344      * @param group (-1 if not a group message)
345      * @return standard message
346      * @throws FieldException
347      * @throws InvalidMessageTypeException
348      */
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;
353         byte f = flags;
354         if (group != -1) {
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));
358         } else {
359             addr = getAddress();
360         }
361         m.setAddress("toAddress", addr);
362         m.setByte("messageFlags", f);
363         m.setByte("command1", cmd1);
364         m.setByte("command2", cmd2);
365         return m;
366     }
367
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);
373         return m;
374     }
375
376     /**
377      * Helper method to make extended message
378      *
379      * @param flags
380      * @param cmd1
381      * @param cmd2
382      * @return extended message
383      * @throws FieldException
384      * @throws InvalidMessageTypeException
385      */
386     public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2)
387             throws FieldException, InvalidMessageTypeException {
388         return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
389     }
390
391     /**
392      * Helper method to make extended message
393      *
394      * @param flags
395      * @param cmd1
396      * @param cmd2
397      * @param data array with userdata
398      * @return extended message
399      * @throws FieldException
400      * @throws InvalidMessageTypeException
401      */
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);
409         m.setUserData(data);
410         m.setCRC();
411         return m;
412     }
413
414     /**
415      * Helper method to make extended message, but with different CRC calculation
416      *
417      * @param flags
418      * @param cmd1
419      * @param cmd2
420      * @param data array with user data
421      * @return extended message
422      * @throws FieldException
423      * @throws InvalidMessageTypeException
424      */
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);
432         m.setUserData(data);
433         m.setCRC2();
434         return m;
435     }
436
437     /**
438      * Called by the RequestQueueManager when the queue has expired
439      *
440      * @param timeNow
441      * @return time when to schedule the next message (timeNow + quietTime)
442      */
443     public long processRequestQueue(long timeNow) {
444         synchronized (mrequestQueue) {
445             if (mrequestQueue.isEmpty()) {
446                 return 0L;
447             }
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());
454                 if (dt < 0) {
455                     logger.debug("still waiting for query reply from {} for another {} usec", address, -dt);
456                     return (timeNow + 2000L); // retry soon
457                 } else {
458                     logger.debug("gave up waiting for query reply from device {}", address);
459                 }
460             }
461             QEntry qe = mrequestQueue.poll(); // take it off the queue!
462             if (qe == null) {
463                 return 0L;
464             }
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();
472             } else {
473                 logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
474             }
475             long quietTime = qe.getMsg().getQuietTime();
476             qe.getMsg().setQuietTime(500L); // rate limiting downstream!
477             try {
478                 writeMessage(qe.getMsg());
479             } catch (IOException e) {
480                 logger.warn("message write failed for msg {}", qe.getMsg(), e);
481             }
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);
487             return (nextTime);
488         }
489     }
490
491     /**
492      * Enqueues message to be sent at the next possible time
493      *
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)
496      */
497     public void enqueueMessage(Msg m, DeviceFeature f) {
498         enqueueDelayedMessage(m, f, 0);
499     }
500
501     /**
502      * Enqueues message to be sent after a delay
503      *
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
507      */
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));
512         }
513         if (!m.isBroadcast()) {
514             m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
515         }
516         logger.trace("enqueing direct message with delay {}", delay);
517         RequestQueueManager instance = RequestQueueManager.instance();
518         if (instance != null) {
519             instance.addQueue(this, now + delay);
520         } else {
521             logger.warn("request queue manger instance is null");
522         }
523     }
524
525     private void writeMessage(Msg m) throws IOException {
526         Driver driver = this.driver;
527         if (driver != null) {
528             driver.writeMessage(m);
529         }
530     }
531
532     private void instantiateFeatures(DeviceType dt) {
533         for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
534             DeviceFeature f = DeviceFeature.makeDeviceFeature(fe.getValue());
535             if (f == null) {
536                 logger.warn("device type {} references unknown feature: {}", dt, fe.getValue());
537             } else {
538                 addFeature(fe.getKey(), f);
539             }
540         }
541         for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
542             FeatureGroup fg = fe.getValue();
543             @Nullable
544             DeviceFeature f = DeviceFeature.makeDeviceFeature(fg.getType());
545             if (f == null) {
546                 logger.warn("device type {} references unknown feature group: {}", dt, fg.getType());
547             } else {
548                 addFeature(fe.getKey(), f);
549                 connectFeatures(fe.getKey(), f, fg.getFeatures());
550             }
551         }
552     }
553
554     private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> fgFeatures) {
555         for (String fs : fgFeatures) {
556             @Nullable
557             DeviceFeature f = features.get(fs);
558             if (f == null) {
559                 logger.warn("feature group {} references unknown feature {}", gn, fs);
560             } else {
561                 logger.debug("{} connected feature: {}", gn, f);
562                 fg.addConnectedFeature(f);
563             }
564         }
565     }
566
567     private void addFeature(String name, DeviceFeature f) {
568         f.setDevice(this);
569         synchronized (features) {
570             features.put(name, f);
571         }
572     }
573
574     /**
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.
578      *
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
583      */
584     public boolean getGroupState(int group, GroupMessage a, byte cmd1) {
585         GroupMessageStateMachine m = groupState.get(group);
586         if (m == null) {
587             m = new GroupMessageStateMachine();
588             groupState.put(group, m);
589             logger.trace("{} created group {} state", address, group);
590         } else {
591             if (lastMsgReceived <= m.getLastUpdated()) {
592                 logger.trace("{} using previous group {} state for {}", address, group, a);
593                 return m.getPublish();
594             }
595         }
596
597         logger.trace("{} updating group {} state to {}", address, group, a);
598         return (m.action(a, address, group, cmd1));
599     }
600
601     @Override
602     public String toString() {
603         String s = address.toString();
604         for (Entry<String, DeviceFeature> f : features.entrySet()) {
605             s += "|" + f.getKey() + "->" + f.getValue().toString();
606         }
607         return s;
608     }
609
610     /**
611      * Factory method
612      *
613      * @param dt device type after which to model the device
614      * @return newly created device
615      */
616     public static InsteonDevice makeDevice(DeviceType dt) {
617         InsteonDevice dev = new InsteonDevice();
618         dev.instantiateFeatures(dt);
619         return dev;
620     }
621
622     /**
623      * Queue entry helper class
624      *
625      * @author Bernd Pfrommer - Initial contribution
626      */
627     public static class QEntry implements Comparable<QEntry> {
628         private DeviceFeature feature;
629         private Msg msg;
630         private long expirationTime;
631
632         public DeviceFeature getFeature() {
633             return feature;
634         }
635
636         public Msg getMsg() {
637             return msg;
638         }
639
640         public long getExpirationTime() {
641             return expirationTime;
642         }
643
644         QEntry(DeviceFeature f, Msg m, long t) {
645             feature = f;
646             msg = m;
647             expirationTime = t;
648         }
649
650         @Override
651         public int compareTo(QEntry a) {
652             return (int) (expirationTime - a.expirationTime);
653         }
654     }
655 }