2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.insteon.internal.message;
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;
21 import java.util.TreeSet;
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;
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.
37 * @author Bernd Pfrommer - Initial contribution
38 * @author Daniel Pfrommer - openHAB 1 insteonplm binding
39 * @author Rob Nielsen - Port to openHAB 2 insteon binding
42 @SuppressWarnings("null")
44 private static final Logger logger = LoggerFactory.getLogger(Msg.class);
47 * Represents the direction of the message from the host's view.
48 * The host is the machine to which the modem is attached.
50 public enum Direction {
52 FROM_MODEM("FROM_MODEM");
54 private static HashMap<String, Direction> map = new HashMap<>();
56 private String directionString;
59 map.put(TO_MODEM.getDirectionString(), TO_MODEM);
60 map.put(FROM_MODEM.getDirectionString(), FROM_MODEM);
63 Direction(String dirString) {
64 this.directionString = dirString;
67 public String getDirectionString() {
68 return directionString;
71 public static Direction getDirectionFromString(String dir) {
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<>();
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;
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)
97 public Msg(int headerLength, byte[] data, int dataLength, Direction dir) {
98 this.headerLength = headerLength;
100 initialize(data, 0, dataLength);
104 * Copy constructor, needed to make a copy of the templates when
105 * generating messages from them.
107 * @param m the message to make a copy of
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;
118 // Use xml msg loader to load configs
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);
125 logger.warn("could not get message definition resource!");
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);
139 // ------------------ simple getters and setters -----------------
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.
147 * @return the time (in milliseconds) to pause after message has been sent
149 public long getQuietTime() {
153 public byte @Nullable [] getData() {
157 public int getLength() {
161 public int getHeaderLength() {
165 public Direction getDirection() {
169 public MsgDefinition getDefinition() {
173 public byte getCommandNumber() {
174 return ((data == null || data.length < 2) ? -1 : data[1]);
177 public boolean isPureNack() {
178 return (data.length == 2 && data[1] == 0x15);
181 public boolean isExtended() {
182 if (data == null || getLength() < 2) {
185 if (!definition.containsField("messageFlags")) {
189 byte flags = getByte("messageFlags");
190 return ((flags & 0x10) == 0x10);
191 } catch (FieldException e) {
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");
203 public boolean isEcho() {
204 return isPureNack() || !isUnsolicited();
207 public boolean isOfType(MsgType mt) {
209 MsgType t = MsgType.fromValue(getByte("messageFlags"));
211 } catch (FieldException e) {
216 public boolean isBroadcast() {
217 return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.BROADCAST);
220 public boolean isCleanup() {
221 return isOfType(MsgType.ALL_LINK_CLEANUP);
224 public boolean isAllLink() {
225 return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
228 public boolean isAckOfDirect() {
229 return isOfType(MsgType.ACK_OF_DIRECT);
232 public boolean isAllLinkCleanupAckOrNack() {
233 return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
236 public boolean isX10() {
238 int cmd = getByte("Cmd") & 0xff;
239 if (cmd == 0x63 || cmd == 0x52) {
242 } catch (FieldException e) {
247 public void setDefinition(MsgDefinition d) {
251 public void setQuietTime(long t) {
255 public void addField(Field f) {
256 definition.addField(f);
259 public @Nullable InsteonAddress getAddr(String name) {
261 InsteonAddress a = null;
263 a = definition.getField(name).getAddress(data);
264 } catch (FieldException e) {
265 // do nothing, we'll return null
270 public int getHopsLeft() throws FieldException {
271 int hops = (getByte("messageFlags") & 0x0c) >> 2;
276 * Will initialize the message with a byte[], an offset, and a length
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
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);
287 logger.warn("intialize(): Offset out of bounds!");
292 * Will put a byte at the specified key
294 * @param key the string key in the message definition
295 * @param value the byte to put
297 public void setByte(@Nullable String key, byte value) throws FieldException {
298 Field f = definition.getField(key);
299 f.setByte(data, value);
303 * Will put an int at the specified field key
305 * @param key the name of the field
306 * @param value the int to put
308 public void setInt(String key, int value) throws FieldException {
309 Field f = definition.getField(key);
310 f.setInt(data, value);
314 * Will put address bytes at the field
316 * @param key the name of the field
317 * @param adr the address to put
319 public void setAddress(String key, InsteonAddress adr) throws FieldException {
320 Field f = definition.getField(key);
321 f.setAddress(data, adr);
327 * @param key the name of the field
330 public byte getByte(String key) throws FieldException {
331 return (definition.getField(key).getByte(data));
335 * Will fetch a byte array starting at a certain field
337 * @param key the name of the first field
338 * @param number of bytes to get
339 * @return the byte array
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!");
346 byte[] section = new byte[numBytes];
347 System.arraycopy(data, offset, section, 0, numBytes);
352 * Will fetch address from field
354 * @param field the filed name to fetch
355 * @return the address
357 public InsteonAddress getAddress(String field) throws FieldException {
358 return (definition.getField(field).getAddress(data));
362 * Fetch 3-byte (24bit) from message
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
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);
375 public String toHexString() {
377 return Utils.getHexString(data);
379 return super.toString();
383 * Sets the userData fields from a byte array
387 public void setUserData(byte[] arg) {
388 byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
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());
410 * Calculate and set the CRC with the older 1-byte method
412 * @return the calculated crc
414 public int setCRC() {
417 crc = getByte("command1") + getByte("command2");
418 byte[] bytes = getBytes("userData1", 13); // skip userData14!
419 for (byte b : bytes) {
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);
432 * Calculate and set the CRC with the newer 2-byte method
434 * @return the calculated crc
436 public int setCRC2() {
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++) {
444 if ((crc & 0x8000) == 0) {
447 if ((crc & 0x4000) == 0) {
450 if ((crc & 0x1000) == 0) {
453 if ((crc & 0x0008) == 0) {
456 crc = ((crc << 1) | fb) & 0xFFFF;
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);
470 public String toString() {
471 String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
473 return toHexString();
475 // need to first sort the fields by offset
476 Comparator<@Nullable Field> cmp = new Comparator<@Nullable Field>() {
478 public int compare(@Nullable Field f1, @Nullable Field f2) {
479 return f1.getOffset() - f2.getOffset();
482 TreeSet<@Nullable Field> fields = new TreeSet<>(cmp);
484 Field f : definition.getFields().values()) {
487 for (Field f : fields) {
488 if (f.getName().equals("messageFlags")) {
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);
500 s += f.toString(data) + "|";
507 * Factory method to create Msg from raw byte stream received from the
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
515 public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
516 if (buf == null || buf.length < 2) {
519 Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
520 if (template == null) {
521 return null; // cannot find lookup map
523 if (msgLen != template.getLength()) {
524 logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
527 Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
528 msg.setDefinition(template.getDefinition());
533 * Finds the header length from the insteon command in the received message
535 * @param cmd the insteon command received in the message
536 * @return the length of the header to expect
538 public static int getHeaderLength(byte cmd) {
539 Integer len = HEADER_MAP.get((int) cmd);
541 return (-1); // not found
547 * Tries to determine the length of a received Insteon message.
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
553 public static int getMessageLength(byte b, boolean isExtended) {
554 int key = cmdToKey(b, isExtended);
555 Msg msg = REPLY_MAP.get(key);
559 return msg.getLength();
563 * From bytes received thus far, tries to determine if an Insteon
564 * message is extended or standard.
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
572 public static boolean isExtended(byte[] buf, int len, int headerLength) {
573 if (headerLength <= 2) {
575 } // extended messages are longer
576 if (len < headerLength) {
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
585 * Creates Insteon message (for sending) of a given type
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
591 public static Msg makeMessage(String type) throws InvalidMessageTypeException {
592 Msg m = MSG_MAP.get(type);
594 throw new InvalidMessageTypeException("unknown message type: " + type);
599 private static int cmdToKey(byte cmd, boolean isExtended) {
600 return (cmd + (isExtended ? 256 : 0));
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());
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);