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