2 * Copyright (c) 2010-2024 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
43 private static final Logger logger = LoggerFactory.getLogger(Msg.class);
46 * Represents the direction of the message from the host's view.
47 * The host is the machine to which the modem is attached.
49 public enum Direction {
51 FROM_MODEM("FROM_MODEM");
53 private static Map<String, Direction> map = new HashMap<>();
55 private String directionString;
58 map.put(TO_MODEM.getDirectionString(), TO_MODEM);
59 map.put(FROM_MODEM.getDirectionString(), FROM_MODEM);
62 Direction(String dirString) {
63 this.directionString = dirString;
66 public String getDirectionString() {
67 return directionString;
70 public static Direction getDirectionFromString(String dir) {
71 Direction direction = map.get(dir);
72 if (direction != null) {
75 throw new IllegalArgumentException("Unable to find direction for " + dir);
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<>();
87 private int headerLength = -1;
89 private MsgDefinition definition = new MsgDefinition();
90 private Direction direction = Direction.TO_MODEM;
91 private long quietTime = 0;
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)
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);
109 * Copy constructor, needed to make a copy of the templates when
110 * generating messages from them.
112 * @param m the message to make a copy of
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;
123 // Use xml msg loader to load configs
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);
130 logger.warn("could not get message definition resource!");
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);
144 // ------------------ simple getters and setters -----------------
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.
152 * @return the time (in milliseconds) to pause after message has been sent
154 public long getQuietTime() {
158 public byte @Nullable [] getData() {
162 public int getLength() {
166 public int getHeaderLength() {
170 public Direction getDirection() {
174 public MsgDefinition getDefinition() {
178 public byte getCommandNumber() {
179 return data.length < 2 ? -1 : data[1];
182 public boolean isPureNack() {
183 return data.length == 2 && data[1] == 0x15;
186 public boolean isExtended() {
187 if (getLength() < 2) {
190 if (!definition.containsField("messageFlags")) {
194 byte flags = getByte("messageFlags");
195 return ((flags & 0x10) == 0x10);
196 } catch (FieldException e) {
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");
208 public boolean isEcho() {
209 return isPureNack() || !isUnsolicited();
212 public boolean isOfType(MsgType mt) {
214 MsgType t = MsgType.fromValue(getByte("messageFlags"));
216 } catch (FieldException e) {
221 public boolean isBroadcast() {
222 return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.BROADCAST);
225 public boolean isCleanup() {
226 return isOfType(MsgType.ALL_LINK_CLEANUP);
229 public boolean isAllLink() {
230 return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
233 public boolean isAckOfDirect() {
234 return isOfType(MsgType.ACK_OF_DIRECT);
237 public boolean isAllLinkCleanupAckOrNack() {
238 return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
241 public boolean isX10() {
243 int cmd = getByte("Cmd") & 0xff;
244 if (cmd == 0x63 || cmd == 0x52) {
247 } catch (FieldException e) {
252 public void setDefinition(MsgDefinition d) {
256 public void setQuietTime(long t) {
260 public void addField(Field f) {
261 definition.addField(f);
264 public @Nullable InsteonAddress getAddr(String name) {
266 InsteonAddress a = null;
268 a = definition.getField(name).getAddress(data);
269 } catch (FieldException e) {
270 // do nothing, we'll return null
275 public int getHopsLeft() throws FieldException {
276 int hops = (getByte("messageFlags") & 0x0c) >> 2;
281 * Will put a byte at the specified key
283 * @param key the string key in the message definition
284 * @param value the byte to put
286 public void setByte(@Nullable String key, byte value) throws FieldException {
287 Field f = definition.getField(key);
288 f.setByte(data, value);
292 * Will put an int at the specified field key
294 * @param key the name of the field
295 * @param value the int to put
297 public void setInt(String key, int value) throws FieldException {
298 Field f = definition.getField(key);
299 f.setInt(data, value);
303 * Will put address bytes at the field
305 * @param key the name of the field
306 * @param adr the address to put
308 public void setAddress(String key, InsteonAddress adr) throws FieldException {
309 Field f = definition.getField(key);
310 f.setAddress(data, adr);
316 * @param key the name of the field
319 public byte getByte(String key) throws FieldException {
320 return (definition.getField(key).getByte(data));
324 * Will fetch a byte array starting at a certain field
326 * @param key the name of the first field
327 * @param numBytes of bytes to get
328 * @return the byte array
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!");
335 byte[] section = new byte[numBytes];
336 byte[] data = this.data;
337 System.arraycopy(data, offset, section, 0, numBytes);
342 * Will fetch address from field
344 * @param field the filed name to fetch
345 * @return the address
347 public InsteonAddress getAddress(String field) throws FieldException {
348 return (definition.getField(field).getAddress(data));
352 * Fetch 3-byte (24bit) from message
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
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);
365 public String toHexString() {
366 return Utils.getHexString(data);
370 * Sets the userData fields from a byte array
374 public void setUserData(byte[] arg) {
375 byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
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());
397 * Calculate and set the CRC with the older 1-byte method
399 * @return the calculated crc
401 public int setCRC() {
404 crc = getByte("command1") + getByte("command2");
405 byte[] bytes = getBytes("userData1", 13); // skip userData14!
406 for (byte b : bytes) {
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);
419 * Calculate and set the CRC with the newer 2-byte method
421 * @return the calculated crc
423 public int setCRC2() {
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++) {
431 if ((crc & 0x8000) == 0) {
434 if ((crc & 0x4000) == 0) {
437 if ((crc & 0x1000) == 0) {
440 if ((crc & 0x0008) == 0) {
443 crc = ((crc << 1) | fb) & 0xFFFF;
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);
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<>() {
462 public int compare(Field f1, Field f2) {
463 return f1.getOffset() - f2.getOffset();
466 TreeSet<Field> fields = new TreeSet<>(cmp);
467 for (Field f : definition.getFields().values()) {
470 for (Field f : fields) {
471 if (f.getName().equals("messageFlags")) {
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);
483 s += f.toString(data) + "|";
490 * Factory method to create Msg from raw byte stream received from the
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
498 public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
499 if (buf.length < 2) {
502 Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
503 if (template == null) {
504 return null; // cannot find lookup map
506 if (msgLen != template.getLength()) {
507 logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
510 Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
511 msg.setDefinition(template.getDefinition());
516 * Finds the header length from the insteon command in the received message
518 * @param cmd the insteon command received in the message
519 * @return the length of the header to expect
521 public static int getHeaderLength(byte cmd) {
522 Integer len = HEADER_MAP.get((int) cmd);
524 return (-1); // not found
530 * Tries to determine the length of a received Insteon message.
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
536 public static int getMessageLength(byte b, boolean isExtended) {
537 int key = cmdToKey(b, isExtended);
538 Msg msg = REPLY_MAP.get(key);
542 return msg.getLength();
546 * From bytes received thus far, tries to determine if an Insteon
547 * message is extended or standard.
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
555 public static boolean isExtended(byte[] buf, int len, int headerLength) {
556 if (headerLength <= 2) {
558 } // extended messages are longer
559 if (len < headerLength) {
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
568 * Creates Insteon message (for sending) of a given type
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
574 public static Msg makeMessage(String type) throws InvalidMessageTypeException {
575 Msg m = MSG_MAP.get(type);
577 throw new InvalidMessageTypeException("unknown message type: " + type);
582 private static int cmdToKey(byte cmd, boolean isExtended) {
583 return (cmd + (isExtended ? 256 : 0));
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());
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);