]> git.basschouten.com Git - openhab-addons.git/blob
698ba8799548006f0d7557aa87dc9a2f5cb8146f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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 @SuppressWarnings("null")
50 public class InsteonDevice {
51     private final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
52
53     public static enum DeviceStatus {
54         INITIALIZED,
55         POLLING
56     }
57
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;
62
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>();
78
79     /**
80      * Constructor
81      */
82     public InsteonDevice() {
83         lastMsgReceived = System.currentTimeMillis();
84     }
85
86     // --------------------- simple getters -----------------------------
87
88     public boolean hasProductKey() {
89         return productKey != null;
90     }
91
92     public @Nullable String getProductKey() {
93         return productKey;
94     }
95
96     public boolean hasModemDBEntry() {
97         return hasModemDBEntry;
98     }
99
100     public DeviceStatus getStatus() {
101         return status;
102     }
103
104     public InsteonAddress getAddress() {
105         return (address);
106     }
107
108     public @Nullable Driver getDriver() {
109         return driver;
110     }
111
112     public long getPollInterval() {
113         return pollInterval;
114     }
115
116     public boolean isModem() {
117         return isModem;
118     }
119
120     public @Nullable DeviceFeature getFeature(String f) {
121         return features.get(f);
122     }
123
124     public HashMap<String, @Nullable DeviceFeature> getFeatures() {
125         return features;
126     }
127
128     public byte getX10HouseCode() {
129         return (address.getX10HouseCode());
130     }
131
132     public byte getX10UnitCode() {
133         return (address.getX10UnitCode());
134     }
135
136     public boolean hasProductKey(String key) {
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, @Nullable Object> deviceConfigMap) {
198         this.deviceConfigMap = deviceConfigMap;
199     }
200
201     public Map<String, @Nullable 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, @Nullable 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().addQueue(this, now + delay);
279
280         if (!l.isEmpty()) {
281             lastTimePolled = now;
282         }
283     }
284
285     /**
286      * Handle incoming message for this device by forwarding
287      * it to all features that this device supports
288      *
289      * @param msg the incoming message
290      */
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);
304                         break;
305                     }
306                 }
307             }
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);
313                 }
314             }
315         }
316     }
317
318     /**
319      * Helper method to make standard message
320      *
321      * @param flags
322      * @param cmd1
323      * @param cmd2
324      * @return standard message
325      * @throws FieldException
326      * @throws IOException
327      */
328     public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2)
329             throws FieldException, InvalidMessageTypeException {
330         return (makeStandardMessage(flags, cmd1, cmd2, -1));
331     }
332
333     /**
334      * Helper method to make standard message, possibly with group
335      *
336      * @param flags
337      * @param cmd1
338      * @param cmd2
339      * @param group (-1 if not a group message)
340      * @return standard message
341      * @throws FieldException
342      * @throws IOException
343      */
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;
348         byte f = flags;
349         if (group != -1) {
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));
353         } else {
354             addr = getAddress();
355         }
356         m.setAddress("toAddress", addr);
357         m.setByte("messageFlags", f);
358         m.setByte("command1", cmd1);
359         m.setByte("command2", cmd2);
360         return m;
361     }
362
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);
368         return m;
369     }
370
371     /**
372      * Helper method to make extended message
373      *
374      * @param flags
375      * @param cmd1
376      * @param cmd2
377      * @return extended message
378      * @throws FieldException
379      * @throws IOException
380      */
381     public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2)
382             throws FieldException, InvalidMessageTypeException {
383         return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
384     }
385
386     /**
387      * Helper method to make extended message
388      *
389      * @param flags
390      * @param cmd1
391      * @param cmd2
392      * @param data array with userdata
393      * @return extended message
394      * @throws FieldException
395      * @throws IOException
396      */
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);
404         m.setUserData(data);
405         m.setCRC();
406         return m;
407     }
408
409     /**
410      * Helper method to make extended message, but with different CRC calculation
411      *
412      * @param flags
413      * @param cmd1
414      * @param cmd2
415      * @param data array with user data
416      * @return extended message
417      * @throws FieldException
418      * @throws IOException
419      */
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);
427         m.setUserData(data);
428         m.setCRC2();
429         return m;
430     }
431
432     /**
433      * Called by the RequestQueueManager when the queue has expired
434      *
435      * @param timeNow
436      * @return time when to schedule the next message (timeNow + quietTime)
437      */
438     public long processRequestQueue(long timeNow) {
439         synchronized (mrequestQueue) {
440             if (mrequestQueue.isEmpty()) {
441                 return 0L;
442             }
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());
448                 if (dt < 0) {
449                     logger.debug("still waiting for query reply from {} for another {} usec", address, -dt);
450                     return (timeNow + 2000L); // retry soon
451                 } else {
452                     logger.debug("gave up waiting for query reply from device {}", address);
453                 }
454             }
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();
463             } else {
464                 logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
465             }
466             long quietTime = qe.getMsg().getQuietTime();
467             qe.getMsg().setQuietTime(500L); // rate limiting downstream!
468             try {
469                 writeMessage(qe.getMsg());
470             } catch (IOException e) {
471                 logger.warn("message write failed for msg {}", qe.getMsg(), e);
472             }
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);
478             return (nextTime);
479         }
480     }
481
482     /**
483      * Enqueues message to be sent at the next possible time
484      *
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)
487      */
488     public void enqueueMessage(Msg m, DeviceFeature f) {
489         enqueueDelayedMessage(m, f, 0);
490     }
491
492     /**
493      * Enqueues message to be sent after a delay
494      *
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
498      */
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));
503         }
504         if (!m.isBroadcast()) {
505             m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
506         }
507         logger.trace("enqueing direct message with delay {}", delay);
508         RequestQueueManager.instance().addQueue(this, now + delay);
509     }
510
511     private void writeMessage(Msg m) throws IOException {
512         driver.writeMessage(m);
513     }
514
515     private void instantiateFeatures(@Nullable DeviceType dt) {
516         for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
517             DeviceFeature f = DeviceFeature.makeDeviceFeature(fe.getValue());
518             if (f == null) {
519                 logger.warn("device type {} references unknown feature: {}", dt, fe.getValue());
520             } else {
521                 addFeature(fe.getKey(), f);
522             }
523         }
524         for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
525             FeatureGroup fg = fe.getValue();
526             @Nullable
527             DeviceFeature f = DeviceFeature.makeDeviceFeature(fg.getType());
528             if (f == null) {
529                 logger.warn("device type {} references unknown feature group: {}", dt, fg.getType());
530             } else {
531                 addFeature(fe.getKey(), f);
532                 connectFeatures(fe.getKey(), f, fg.getFeatures());
533             }
534         }
535     }
536
537     private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> fgFeatures) {
538         for (String fs : fgFeatures) {
539             @Nullable
540             DeviceFeature f = features.get(fs);
541             if (f == null) {
542                 logger.warn("feature group {} references unknown feature {}", gn, fs);
543             } else {
544                 logger.debug("{} connected feature: {}", gn, f);
545                 fg.addConnectedFeature(f);
546             }
547         }
548     }
549
550     private void addFeature(String name, DeviceFeature f) {
551         f.setDevice(this);
552         synchronized (features) {
553             features.put(name, f);
554         }
555     }
556
557     /**
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.
561      *
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
566      */
567     public boolean getGroupState(int group, GroupMessage a, byte cmd1) {
568         GroupMessageStateMachine m = groupState.get(group);
569         if (m == null) {
570             m = new GroupMessageStateMachine();
571             groupState.put(group, m);
572             logger.trace("{} created group {} state", address, group);
573         } else {
574             if (lastMsgReceived <= m.getLastUpdated()) {
575                 logger.trace("{} using previous group {} state for {}", address, group, a);
576                 return m.getPublish();
577             }
578         }
579
580         logger.trace("{} updating group {} state to {}", address, group, a);
581         return (m.action(a, address, group, cmd1));
582     }
583
584     @Override
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();
589         }
590         return s;
591     }
592
593     /**
594      * Factory method
595      *
596      * @param dt device type after which to model the device
597      * @return newly created device
598      */
599     public static InsteonDevice makeDevice(@Nullable DeviceType dt) {
600         InsteonDevice dev = new InsteonDevice();
601         dev.instantiateFeatures(dt);
602         return dev;
603     }
604
605     /**
606      * Queue entry helper class
607      *
608      * @author Bernd Pfrommer - Initial contribution
609      */
610     @NonNullByDefault
611     public static class QEntry implements Comparable<QEntry> {
612         private DeviceFeature feature;
613         private Msg msg;
614         private long expirationTime;
615
616         public DeviceFeature getFeature() {
617             return feature;
618         }
619
620         public Msg getMsg() {
621             return msg;
622         }
623
624         public long getExpirationTime() {
625             return expirationTime;
626         }
627
628         QEntry(DeviceFeature f, Msg m, long t) {
629             feature = f;
630             msg = m;
631             expirationTime = t;
632         }
633
634         @Override
635         public int compareTo(QEntry a) {
636             return (int) (expirationTime - a.expirationTime);
637         }
638     }
639 }