]> git.basschouten.com Git - openhab-addons.git/blob
67767228132827f456078e493f3d4358324bd3e4
[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.message;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.Arrays;
18 import java.util.Comparator;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.TreeSet;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.insteon.internal.device.InsteonAddress;
26 import org.openhab.binding.insteon.internal.utils.Utils;
27 import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
28 import org.osgi.framework.FrameworkUtil;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * Contains an Insteon Message consisting of the raw data, and the message definition.
34  * For more info, see the public Insteon Developer's Guide, 2nd edition,
35  * and the Insteon Modem Developer's Guide.
36  *
37  * @author Bernd Pfrommer - Initial contribution
38  * @author Daniel Pfrommer - openHAB 1 insteonplm binding
39  * @author Rob Nielsen - Port to openHAB 2 insteon binding
40  */
41 @NonNullByDefault
42 @SuppressWarnings("null")
43 public class Msg {
44     private static final Logger logger = LoggerFactory.getLogger(Msg.class);
45
46     /**
47      * Represents the direction of the message from the host's view.
48      * The host is the machine to which the modem is attached.
49      */
50     public enum Direction {
51         TO_MODEM("TO_MODEM"),
52         FROM_MODEM("FROM_MODEM");
53
54         private static HashMap<String, Direction> map = new HashMap<>();
55
56         private String directionString;
57
58         static {
59             map.put(TO_MODEM.getDirectionString(), TO_MODEM);
60             map.put(FROM_MODEM.getDirectionString(), FROM_MODEM);
61         }
62
63         Direction(String dirString) {
64             this.directionString = dirString;
65         }
66
67         public String getDirectionString() {
68             return directionString;
69         }
70
71         public static Direction getDirectionFromString(String dir) {
72             return map.get(dir);
73         }
74     }
75
76     // has the structure of all known messages
77     private static final Map<String, @Nullable Msg> MSG_MAP = new HashMap<>();
78     // maps between command number and the length of the header
79     private static final Map<Integer, @Nullable Integer> HEADER_MAP = new HashMap<>();
80     // has templates for all message from modem to host
81     private static final Map<Integer, @Nullable Msg> REPLY_MAP = new HashMap<>();
82
83     private int headerLength = -1;
84     private byte @Nullable [] data = null;
85     private MsgDefinition definition = new MsgDefinition();
86     private Direction direction = Direction.TO_MODEM;
87     private long quietTime = 0;
88
89     /**
90      * Constructor
91      *
92      * @param headerLength length of message header (in bytes)
93      * @param data byte array with message
94      * @param dataLength length of byte array data (in bytes)
95      * @param dir direction of the message (from/to modem)
96      */
97     public Msg(int headerLength, byte[] data, int dataLength, Direction dir) {
98         this.headerLength = headerLength;
99         this.direction = dir;
100         initialize(data, 0, dataLength);
101     }
102
103     /**
104      * Copy constructor, needed to make a copy of the templates when
105      * generating messages from them.
106      *
107      * @param m the message to make a copy of
108      */
109     public Msg(Msg m) {
110         headerLength = m.headerLength;
111         data = m.data.clone();
112         // the message definition usually doesn't change, but just to be sure...
113         definition = new MsgDefinition(m.definition);
114         direction = m.direction;
115     }
116
117     static {
118         // Use xml msg loader to load configs
119         try {
120             InputStream stream = FrameworkUtil.getBundle(Msg.class).getResource("/msg_definitions.xml").openStream();
121             if (stream != null) {
122                 HashMap<String, Msg> msgs = XMLMessageReader.readMessageDefinitions(stream);
123                 MSG_MAP.putAll(msgs);
124             } else {
125                 logger.warn("could not get message definition resource!");
126             }
127         } catch (IOException e) {
128             logger.warn("i/o error parsing xml insteon message definitions", e);
129         } catch (ParsingException e) {
130             logger.warn("parse error parsing xml insteon message definitions", e);
131         } catch (FieldException e) {
132             logger.warn("got field exception while parsing xml insteon message definitions", e);
133         }
134         buildHeaderMap();
135         buildLengthMap();
136     }
137
138     //
139     // ------------------ simple getters and setters -----------------
140     //
141
142     /**
143      * Experience has shown that if Insteon messages are sent in close succession,
144      * only the first one will make it. The quiet time parameter says how long to
145      * wait after a message before the next one can be sent.
146      *
147      * @return the time (in milliseconds) to pause after message has been sent
148      */
149     public long getQuietTime() {
150         return quietTime;
151     }
152
153     public byte @Nullable [] getData() {
154         return data;
155     }
156
157     public int getLength() {
158         return data.length;
159     }
160
161     public int getHeaderLength() {
162         return headerLength;
163     }
164
165     public Direction getDirection() {
166         return direction;
167     }
168
169     public MsgDefinition getDefinition() {
170         return definition;
171     }
172
173     public byte getCommandNumber() {
174         return ((data == null || data.length < 2) ? -1 : data[1]);
175     }
176
177     public boolean isPureNack() {
178         return (data.length == 2 && data[1] == 0x15);
179     }
180
181     public boolean isExtended() {
182         if (data == null || getLength() < 2) {
183             return false;
184         }
185         if (!definition.containsField("messageFlags")) {
186             return (false);
187         }
188         try {
189             byte flags = getByte("messageFlags");
190             return ((flags & 0x10) == 0x10);
191         } catch (FieldException e) {
192             // do nothing
193         }
194         return false;
195     }
196
197     public boolean isUnsolicited() {
198         // if the message has an ACK/NACK, it is in response to our message,
199         // otherwise it is out-of-band, i.e. unsolicited
200         return !definition.containsField("ACK/NACK");
201     }
202
203     public boolean isEcho() {
204         return isPureNack() || !isUnsolicited();
205     }
206
207     public boolean isOfType(MsgType mt) {
208         try {
209             MsgType t = MsgType.fromValue(getByte("messageFlags"));
210             return (t == mt);
211         } catch (FieldException e) {
212             return false;
213         }
214     }
215
216     public boolean isBroadcast() {
217         return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.BROADCAST);
218     }
219
220     public boolean isCleanup() {
221         return isOfType(MsgType.ALL_LINK_CLEANUP);
222     }
223
224     public boolean isAllLink() {
225         return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
226     }
227
228     public boolean isAckOfDirect() {
229         return isOfType(MsgType.ACK_OF_DIRECT);
230     }
231
232     public boolean isAllLinkCleanupAckOrNack() {
233         return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
234     }
235
236     public boolean isX10() {
237         try {
238             int cmd = getByte("Cmd") & 0xff;
239             if (cmd == 0x63 || cmd == 0x52) {
240                 return true;
241             }
242         } catch (FieldException e) {
243         }
244         return false;
245     }
246
247     public void setDefinition(MsgDefinition d) {
248         definition = d;
249     }
250
251     public void setQuietTime(long t) {
252         quietTime = t;
253     }
254
255     public void addField(Field f) {
256         definition.addField(f);
257     }
258
259     public @Nullable InsteonAddress getAddr(String name) {
260         @Nullable
261         InsteonAddress a = null;
262         try {
263             a = definition.getField(name).getAddress(data);
264         } catch (FieldException e) {
265             // do nothing, we'll return null
266         }
267         return a;
268     }
269
270     public int getHopsLeft() throws FieldException {
271         int hops = (getByte("messageFlags") & 0x0c) >> 2;
272         return hops;
273     }
274
275     /**
276      * Will initialize the message with a byte[], an offset, and a length
277      *
278      * @param newData the src byte array
279      * @param offset the offset in the src byte array
280      * @param len the length to copy from the src byte array
281      */
282     private void initialize(byte[] newData, int offset, int len) {
283         data = new byte[len];
284         if (offset >= 0 && offset < newData.length) {
285             System.arraycopy(newData, offset, data, 0, len);
286         } else {
287             logger.warn("intialize(): Offset out of bounds!");
288         }
289     }
290
291     /**
292      * Will put a byte at the specified key
293      *
294      * @param key the string key in the message definition
295      * @param value the byte to put
296      */
297     public void setByte(@Nullable String key, byte value) throws FieldException {
298         Field f = definition.getField(key);
299         f.setByte(data, value);
300     }
301
302     /**
303      * Will put an int at the specified field key
304      *
305      * @param key the name of the field
306      * @param value the int to put
307      */
308     public void setInt(String key, int value) throws FieldException {
309         Field f = definition.getField(key);
310         f.setInt(data, value);
311     }
312
313     /**
314      * Will put address bytes at the field
315      *
316      * @param key the name of the field
317      * @param adr the address to put
318      */
319     public void setAddress(String key, InsteonAddress adr) throws FieldException {
320         Field f = definition.getField(key);
321         f.setAddress(data, adr);
322     }
323
324     /**
325      * Will fetch a byte
326      *
327      * @param key the name of the field
328      * @return the byte
329      */
330     public byte getByte(String key) throws FieldException {
331         return (definition.getField(key).getByte(data));
332     }
333
334     /**
335      * Will fetch a byte array starting at a certain field
336      *
337      * @param key the name of the first field
338      * @param number of bytes to get
339      * @return the byte array
340      */
341     public byte[] getBytes(String key, int numBytes) throws FieldException {
342         int offset = definition.getField(key).getOffset();
343         if (offset < 0 || offset + numBytes > data.length) {
344             throw new FieldException("data index out of bounds!");
345         }
346         byte[] section = new byte[numBytes];
347         System.arraycopy(data, offset, section, 0, numBytes);
348         return section;
349     }
350
351     /**
352      * Will fetch address from field
353      *
354      * @param field the filed name to fetch
355      * @return the address
356      */
357     public InsteonAddress getAddress(String field) throws FieldException {
358         return (definition.getField(field).getAddress(data));
359     }
360
361     /**
362      * Fetch 3-byte (24bit) from message
363      *
364      * @param key1 the key of the msb
365      * @param key2 the key of the second msb
366      * @param key3 the key of the lsb
367      * @return the integer
368      */
369     public int getInt24(String key1, String key2, String key3) throws FieldException {
370         int i = (definition.getField(key1).getByte(data) << 16) & (definition.getField(key2).getByte(data) << 8)
371                 & definition.getField(key3).getByte(data);
372         return i;
373     }
374
375     public String toHexString() {
376         if (data != null) {
377             return Utils.getHexString(data);
378         }
379         return super.toString();
380     }
381
382     /**
383      * Sets the userData fields from a byte array
384      *
385      * @param data
386      */
387     public void setUserData(byte[] arg) {
388         byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
389         try {
390             setByte("userData1", data[0]);
391             setByte("userData2", data[1]);
392             setByte("userData3", data[2]);
393             setByte("userData4", data[3]);
394             setByte("userData5", data[4]);
395             setByte("userData6", data[5]);
396             setByte("userData7", data[6]);
397             setByte("userData8", data[7]);
398             setByte("userData9", data[8]);
399             setByte("userData10", data[9]);
400             setByte("userData11", data[10]);
401             setByte("userData12", data[11]);
402             setByte("userData13", data[12]);
403             setByte("userData14", data[13]);
404         } catch (FieldException e) {
405             logger.warn("got field exception on msg {}:", e.getMessage());
406         }
407     }
408
409     /**
410      * Calculate and set the CRC with the older 1-byte method
411      *
412      * @return the calculated crc
413      */
414     public int setCRC() {
415         int crc;
416         try {
417             crc = getByte("command1") + getByte("command2");
418             byte[] bytes = getBytes("userData1", 13); // skip userData14!
419             for (byte b : bytes) {
420                 crc += b;
421             }
422             crc = ((~crc) + 1) & 0xFF;
423             setByte("userData14", (byte) (crc & 0xFF));
424         } catch (FieldException e) {
425             logger.warn("got field exception on msg {}:", this, e);
426             crc = 0;
427         }
428         return crc;
429     }
430
431     /**
432      * Calculate and set the CRC with the newer 2-byte method
433      *
434      * @return the calculated crc
435      */
436     public int setCRC2() {
437         int crc = 0;
438         try {
439             byte[] bytes = getBytes("command1", 14);
440             for (int loop = 0; loop < bytes.length; loop++) {
441                 int b = bytes[loop] & 0xFF;
442                 for (int bit = 0; bit < 8; bit++) {
443                     int fb = b & 0x01;
444                     if ((crc & 0x8000) == 0) {
445                         fb = fb ^ 0x01;
446                     }
447                     if ((crc & 0x4000) == 0) {
448                         fb = fb ^ 0x01;
449                     }
450                     if ((crc & 0x1000) == 0) {
451                         fb = fb ^ 0x01;
452                     }
453                     if ((crc & 0x0008) == 0) {
454                         fb = fb ^ 0x01;
455                     }
456                     crc = ((crc << 1) | fb) & 0xFFFF;
457                     b = b >> 1;
458                 }
459             }
460             setByte("userData13", (byte) ((crc >> 8) & 0xFF));
461             setByte("userData14", (byte) (crc & 0xFF));
462         } catch (FieldException e) {
463             logger.warn("got field exception on msg {}:", this, e);
464             crc = 0;
465         }
466         return crc;
467     }
468
469     @Override
470     public String toString() {
471         String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
472         if (data == null) {
473             return toHexString();
474         }
475         // need to first sort the fields by offset
476         Comparator<@Nullable Field> cmp = new Comparator<@Nullable Field>() {
477             @Override
478             public int compare(@Nullable Field f1, @Nullable Field f2) {
479                 return f1.getOffset() - f2.getOffset();
480             }
481         };
482         TreeSet<@Nullable Field> fields = new TreeSet<>(cmp);
483         for (@Nullable
484         Field f : definition.getFields().values()) {
485             fields.add(f);
486         }
487         for (Field f : fields) {
488             if (f.getName().equals("messageFlags")) {
489                 byte b;
490                 try {
491                     b = f.getByte(data);
492                     MsgType t = MsgType.fromValue(b);
493                     s += f.toString(data) + "=" + t.toString() + ":" + (b & 0x03) + ":" + ((b & 0x0c) >> 2) + "|";
494                 } catch (FieldException e) {
495                     logger.warn("toString error: ", e);
496                 } catch (IllegalArgumentException e) {
497                     logger.warn("toString msg type error: ", e);
498                 }
499             } else {
500                 s += f.toString(data) + "|";
501             }
502         }
503         return s;
504     }
505
506     /**
507      * Factory method to create Msg from raw byte stream received from the
508      * serial port.
509      *
510      * @param buf the raw received bytes
511      * @param msgLen length of received buffer
512      * @param isExtended whether it is an extended message or not
513      * @return message, or null if the Msg cannot be created
514      */
515     public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
516         if (buf == null || buf.length < 2) {
517             return null;
518         }
519         Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
520         if (template == null) {
521             return null; // cannot find lookup map
522         }
523         if (msgLen != template.getLength()) {
524             logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
525             return null;
526         }
527         Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
528         msg.setDefinition(template.getDefinition());
529         return (msg);
530     }
531
532     /**
533      * Finds the header length from the insteon command in the received message
534      *
535      * @param cmd the insteon command received in the message
536      * @return the length of the header to expect
537      */
538     public static int getHeaderLength(byte cmd) {
539         Integer len = HEADER_MAP.get((int) cmd);
540         if (len == null) {
541             return (-1); // not found
542         }
543         return len;
544     }
545
546     /**
547      * Tries to determine the length of a received Insteon message.
548      *
549      * @param b Insteon message command received
550      * @param isExtended flag indicating if it is an extended message
551      * @return message length, or -1 if length cannot be determined
552      */
553     public static int getMessageLength(byte b, boolean isExtended) {
554         int key = cmdToKey(b, isExtended);
555         Msg msg = REPLY_MAP.get(key);
556         if (msg == null) {
557             return -1;
558         }
559         return msg.getLength();
560     }
561
562     /**
563      * From bytes received thus far, tries to determine if an Insteon
564      * message is extended or standard.
565      *
566      * @param buf the received bytes
567      * @param len the number of bytes received so far
568      * @param headerLength the known length of the header
569      * @return true if it is definitely extended, false if cannot be
570      *         determined or if it is a standard message
571      */
572     public static boolean isExtended(byte[] buf, int len, int headerLength) {
573         if (headerLength <= 2) {
574             return false;
575         } // extended messages are longer
576         if (len < headerLength) {
577             return false;
578         } // not enough data to tell if extended
579         byte flags = buf[headerLength - 1]; // last byte says flags
580         boolean isExtended = (flags & 0x10) == 0x10; // bit 4 is the message
581         return (isExtended);
582     }
583
584     /**
585      * Creates Insteon message (for sending) of a given type
586      *
587      * @param type the type of message to create, as defined in the xml file
588      * @return reference to message created
589      * @throws IOException if there is no such message type known
590      */
591     public static Msg makeMessage(String type) throws InvalidMessageTypeException {
592         Msg m = MSG_MAP.get(type);
593         if (m == null) {
594             throw new InvalidMessageTypeException("unknown message type: " + type);
595         }
596         return new Msg(m);
597     }
598
599     private static int cmdToKey(byte cmd, boolean isExtended) {
600         return (cmd + (isExtended ? 256 : 0));
601     }
602
603     private static void buildHeaderMap() {
604         for (Msg m : MSG_MAP.values()) {
605             if (m.getDirection() == Direction.FROM_MODEM) {
606                 HEADER_MAP.put((int) m.getCommandNumber(), m.getHeaderLength());
607             }
608         }
609     }
610
611     private static void buildLengthMap() {
612         for (Msg m : MSG_MAP.values()) {
613             if (m.getDirection() == Direction.FROM_MODEM) {
614                 int key = cmdToKey(m.getCommandNumber(), m.isExtended());
615                 REPLY_MAP.put(key, m);
616             }
617         }
618     }
619 }