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, Msg> MSG_MAP = new HashMap<>();
78 // maps between command number and the length of the header
79 private static final Map<Integer, Integer> HEADER_MAP = new HashMap<>();
80 // has templates for all message from modem to host
81 private static final Map<Integer, 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 byte[] 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!");
293 * Will put a byte at the specified key
295 * @param key the string key in the message definition
296 * @param value the byte to put
298 public void setByte(@Nullable String key, byte value) throws FieldException {
299 Field f = definition.getField(key);
300 f.setByte(data, value);
304 * Will put an int at the specified field key
306 * @param key the name of the field
307 * @param value the int to put
309 public void setInt(String key, int value) throws FieldException {
310 Field f = definition.getField(key);
311 f.setInt(data, value);
315 * Will put address bytes at the field
317 * @param key the name of the field
318 * @param adr the address to put
320 public void setAddress(String key, InsteonAddress adr) throws FieldException {
321 Field f = definition.getField(key);
322 f.setAddress(data, adr);
328 * @param key the name of the field
331 public byte getByte(String key) throws FieldException {
332 return (definition.getField(key).getByte(data));
336 * Will fetch a byte array starting at a certain field
338 * @param key the name of the first field
339 * @param number of bytes to get
340 * @return the byte array
342 public byte[] getBytes(String key, int numBytes) throws FieldException {
343 int offset = definition.getField(key).getOffset();
344 if (offset < 0 || offset + numBytes > data.length) {
345 throw new FieldException("data index out of bounds!");
347 byte[] section = new byte[numBytes];
348 byte[] data = this.data;
350 System.arraycopy(data, offset, section, 0, numBytes);
356 * Will fetch address from field
358 * @param field the filed name to fetch
359 * @return the address
361 public InsteonAddress getAddress(String field) throws FieldException {
362 return (definition.getField(field).getAddress(data));
366 * Fetch 3-byte (24bit) from message
368 * @param key1 the key of the msb
369 * @param key2 the key of the second msb
370 * @param key3 the key of the lsb
371 * @return the integer
373 public int getInt24(String key1, String key2, String key3) throws FieldException {
374 int i = (definition.getField(key1).getByte(data) << 16) & (definition.getField(key2).getByte(data) << 8)
375 & definition.getField(key3).getByte(data);
379 public String toHexString() {
381 return Utils.getHexString(data);
383 return super.toString();
387 * Sets the userData fields from a byte array
391 public void setUserData(byte[] arg) {
392 byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
394 setByte("userData1", data[0]);
395 setByte("userData2", data[1]);
396 setByte("userData3", data[2]);
397 setByte("userData4", data[3]);
398 setByte("userData5", data[4]);
399 setByte("userData6", data[5]);
400 setByte("userData7", data[6]);
401 setByte("userData8", data[7]);
402 setByte("userData9", data[8]);
403 setByte("userData10", data[9]);
404 setByte("userData11", data[10]);
405 setByte("userData12", data[11]);
406 setByte("userData13", data[12]);
407 setByte("userData14", data[13]);
408 } catch (FieldException e) {
409 logger.warn("got field exception on msg {}:", e.getMessage());
414 * Calculate and set the CRC with the older 1-byte method
416 * @return the calculated crc
418 public int setCRC() {
421 crc = getByte("command1") + getByte("command2");
422 byte[] bytes = getBytes("userData1", 13); // skip userData14!
423 for (byte b : bytes) {
426 crc = ((~crc) + 1) & 0xFF;
427 setByte("userData14", (byte) (crc & 0xFF));
428 } catch (FieldException e) {
429 logger.warn("got field exception on msg {}:", this, e);
436 * Calculate and set the CRC with the newer 2-byte method
438 * @return the calculated crc
440 public int setCRC2() {
443 byte[] bytes = getBytes("command1", 14);
444 for (int loop = 0; loop < bytes.length; loop++) {
445 int b = bytes[loop] & 0xFF;
446 for (int bit = 0; bit < 8; bit++) {
448 if ((crc & 0x8000) == 0) {
451 if ((crc & 0x4000) == 0) {
454 if ((crc & 0x1000) == 0) {
457 if ((crc & 0x0008) == 0) {
460 crc = ((crc << 1) | fb) & 0xFFFF;
464 setByte("userData13", (byte) ((crc >> 8) & 0xFF));
465 setByte("userData14", (byte) (crc & 0xFF));
466 } catch (FieldException e) {
467 logger.warn("got field exception on msg {}:", this, e);
474 public String toString() {
475 String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
477 return toHexString();
479 // need to first sort the fields by offset
480 Comparator<@Nullable Field> cmp = new Comparator<@Nullable Field>() {
482 public int compare(@Nullable Field f1, @Nullable Field f2) {
483 return f1.getOffset() - f2.getOffset();
486 TreeSet<@Nullable Field> fields = new TreeSet<>(cmp);
488 Field f : definition.getFields().values()) {
491 for (Field f : fields) {
492 if (f.getName().equals("messageFlags")) {
496 MsgType t = MsgType.fromValue(b);
497 s += f.toString(data) + "=" + t.toString() + ":" + (b & 0x03) + ":" + ((b & 0x0c) >> 2) + "|";
498 } catch (FieldException e) {
499 logger.warn("toString error: ", e);
500 } catch (IllegalArgumentException e) {
501 logger.warn("toString msg type error: ", e);
504 s += f.toString(data) + "|";
511 * Factory method to create Msg from raw byte stream received from the
514 * @param buf the raw received bytes
515 * @param msgLen length of received buffer
516 * @param isExtended whether it is an extended message or not
517 * @return message, or null if the Msg cannot be created
519 public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
520 if (buf == null || buf.length < 2) {
523 Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
524 if (template == null) {
525 return null; // cannot find lookup map
527 if (msgLen != template.getLength()) {
528 logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
531 Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
532 msg.setDefinition(template.getDefinition());
537 * Finds the header length from the insteon command in the received message
539 * @param cmd the insteon command received in the message
540 * @return the length of the header to expect
542 public static int getHeaderLength(byte cmd) {
543 Integer len = HEADER_MAP.get((int) cmd);
545 return (-1); // not found
551 * Tries to determine the length of a received Insteon message.
553 * @param b Insteon message command received
554 * @param isExtended flag indicating if it is an extended message
555 * @return message length, or -1 if length cannot be determined
557 public static int getMessageLength(byte b, boolean isExtended) {
558 int key = cmdToKey(b, isExtended);
559 Msg msg = REPLY_MAP.get(key);
563 return msg.getLength();
567 * From bytes received thus far, tries to determine if an Insteon
568 * message is extended or standard.
570 * @param buf the received bytes
571 * @param len the number of bytes received so far
572 * @param headerLength the known length of the header
573 * @return true if it is definitely extended, false if cannot be
574 * determined or if it is a standard message
576 public static boolean isExtended(byte[] buf, int len, int headerLength) {
577 if (headerLength <= 2) {
579 } // extended messages are longer
580 if (len < headerLength) {
582 } // not enough data to tell if extended
583 byte flags = buf[headerLength - 1]; // last byte says flags
584 boolean isExtended = (flags & 0x10) == 0x10; // bit 4 is the message
589 * Creates Insteon message (for sending) of a given type
591 * @param type the type of message to create, as defined in the xml file
592 * @return reference to message created
593 * @throws IOException if there is no such message type known
595 public static Msg makeMessage(String type) throws InvalidMessageTypeException {
596 Msg m = MSG_MAP.get(type);
598 throw new InvalidMessageTypeException("unknown message type: " + type);
603 private static int cmdToKey(byte cmd, boolean isExtended) {
604 return (cmd + (isExtended ? 256 : 0));
607 private static void buildHeaderMap() {
608 for (Msg m : MSG_MAP.values()) {
609 if (m.getDirection() == Direction.FROM_MODEM) {
610 HEADER_MAP.put((int) m.getCommandNumber(), m.getHeaderLength());
615 private static void buildLengthMap() {
616 for (Msg m : MSG_MAP.values()) {
617 if (m.getDirection() == Direction.FROM_MODEM) {
618 int key = cmdToKey(m.getCommandNumber(), m.isExtended());
619 REPLY_MAP.put(key, m);