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.transport.message;
15 import java.util.Arrays;
16 import java.util.Optional;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.insteon.internal.device.DeviceAddress;
21 import org.openhab.binding.insteon.internal.device.InsteonAddress;
22 import org.openhab.binding.insteon.internal.device.X10Address;
23 import org.openhab.binding.insteon.internal.device.X10Flag;
24 import org.openhab.binding.insteon.internal.utils.BinaryUtils;
25 import org.openhab.binding.insteon.internal.utils.HexUtils;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
30 * Contains an Insteon Message consisting of the raw data, and the message definition.
31 * For more info, see the public Insteon Developer's Guide, 2nd edition,
32 * and the Insteon Modem Developer's Guide.
34 * @author Bernd Pfrommer - Initial contribution
35 * @author Daniel Pfrommer - openHAB 1 insteonplm binding
36 * @author Rob Nielsen - Port to openHAB 2 insteon binding
37 * @author Jeremy Setton - Rewrite insteon binding
42 public static enum Direction {
47 private final Logger logger = LoggerFactory.getLogger(Msg.class);
50 private int headerLength;
51 private Direction direction;
52 private MsgDefinition definition = new MsgDefinition();
53 private long quietTime = 0;
54 private boolean replayed = false;
55 private long timestamp = System.currentTimeMillis();
57 public Msg(int headerLength, int dataLength, Direction direction) {
58 this.data = new byte[dataLength];
59 this.headerLength = headerLength;
60 this.direction = direction;
63 public Msg(Msg msg, byte[] data, int dataLength) {
64 this.data = Arrays.copyOf(data, dataLength);
65 this.headerLength = msg.headerLength;
66 this.direction = msg.direction;
67 // the message definition usually doesn't change, but just to be sure...
68 this.definition = new MsgDefinition(msg.definition);
72 this(msg, msg.data, msg.data.length);
75 public byte[] getData() {
79 public int getLength() {
83 public int getHeaderLength() {
87 public Direction getDirection() {
91 public MsgDefinition getDefinition() {
95 public long getQuietTime() {
99 public byte getCommand() {
101 return getByte("Cmd");
102 } catch (FieldException e) {
107 public long getTimestamp() {
111 public boolean isPureNack() {
112 return data.length == 2 && data[1] == 0x15;
115 public boolean isExtended() {
117 return BinaryUtils.isBitSet(getInt("messageFlags"), 4);
118 } catch (FieldException e) {
123 public boolean isFromAddress(@Nullable InsteonAddress address) {
125 return getInsteonAddress("fromAddress").equals(address);
126 } catch (FieldException e) {
131 public boolean isInbound() {
132 return direction == Direction.FROM_MODEM;
135 public boolean isOutbound() {
136 return direction == Direction.TO_MODEM;
139 public boolean isEcho() {
140 return isPureNack() || isReply();
143 public boolean isReply() {
144 return containsField("ACK/NACK");
147 public boolean isReplyAck() {
149 return getByte("ACK/NACK") == 0x06;
150 } catch (FieldException e) {
155 public boolean isReplyNack() {
157 return getByte("ACK/NACK") == 0x15;
158 } catch (FieldException e) {
163 public boolean isReplyOf(Msg msg) {
164 return isReply() && Arrays.equals(msg.getData(), Arrays.copyOf(data, msg.getLength()));
167 public boolean isFailureReport() {
168 return getCommand() == 0x5C;
171 public boolean isOfType(MsgType type) {
172 return type == getType();
175 public boolean isBroadcast() {
176 return isOfType(MsgType.BROADCAST);
179 public boolean isAllLinkBroadcast() {
180 return isOfType(MsgType.ALL_LINK_BROADCAST);
183 public boolean isAllLinkCleanup() {
184 return isOfType(MsgType.ALL_LINK_CLEANUP);
187 public boolean isAllLinkBroadcastOrCleanup() {
188 return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
191 public boolean isAllLinkCleanupAckOrNack() {
192 return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
195 public boolean isAllLinkSuccessReport() {
197 return isOfType(MsgType.ALL_LINK_BROADCAST) && getByte("command1") == 0x06;
198 } catch (FieldException e) {
203 public boolean isDirect() {
204 return isOfType(MsgType.DIRECT);
207 public boolean isAckOfDirect() {
208 return isOfType(MsgType.ACK_OF_DIRECT);
211 public boolean isNackOfDirect() {
212 return isOfType(MsgType.NACK_OF_DIRECT);
215 public boolean isAckOrNackOfDirect() {
216 return isOfType(MsgType.ACK_OF_DIRECT) || isOfType(MsgType.NACK_OF_DIRECT);
219 public boolean isInsteon() {
220 return containsField("messageFlags");
223 public boolean isX10() {
224 return containsField("X10Flag");
227 public boolean isX10Address() {
229 return getByte("X10Flag") == X10Flag.ADDRESS.code();
230 } catch (FieldException e) {
235 public boolean isX10Command() {
237 return getByte("X10Flag") == X10Flag.COMMAND.code();
238 } catch (FieldException e) {
243 public boolean isReplayed() {
247 public void setDefinition(MsgDefinition definition) {
248 this.definition = definition;
251 public void setQuietTime(long quietTime) {
252 this.quietTime = quietTime;
255 public void setIsReplayed(boolean replayed) {
256 this.replayed = replayed;
259 public void addField(Field f) {
260 definition.addField(f);
263 public boolean containsField(String key) {
264 return definition.containsField(key);
267 public int getHopsLeft() {
269 return (getByte("messageFlags") & 0x0C) >> 2;
270 } catch (FieldException e) {
275 public int getMaxHops() {
277 return getByte("messageFlags") & 0x03;
278 } catch (FieldException e) {
284 * Sets a byte at a specific field
286 * @param key the string key in the message definition
287 * @param value the byte to put
289 public void setByte(String key, byte value) throws FieldException {
290 Field field = definition.getField(key);
291 field.setByte(data, value);
295 * Sets address bytes at a specific field
297 * @param key the name of the field
298 * @param address the address to put
300 public void setAddress(String key, DeviceAddress address) throws FieldException {
301 Field field = definition.getField(key);
302 if (address instanceof InsteonAddress insteonAddress) {
303 field.setAddress(data, insteonAddress);
304 } else if (address instanceof X10Address x10Address) {
305 field.setByte(data, x10Address.getCode());
310 * Sets a byte array starting at a specific field
312 * @param key the name of the first field
314 public void setBytes(String key, byte[] bytes) throws FieldException {
315 int offset = definition.getField(key).getOffset();
316 if (offset < 0 || offset + bytes.length > data.length) {
317 throw new FieldException("data index out of bounds!");
319 System.arraycopy(bytes, 0, data, offset, bytes.length);
323 * Sets a byte array starting at a specific field as an up to 32-bit integer
325 * @param key the name of the first field
326 * @param value the integer to put
327 * @param numBytes number of bytes to put
329 public void setInt(String key, int value, int numBytes) throws FieldException {
330 if (numBytes < 1 || numBytes > 4) {
331 throw new FieldException("number of bytes out of bounds!");
333 byte[] bytes = new byte[numBytes];
334 int shift = 8 * (numBytes - 1);
335 for (int i = 0; i < numBytes; i++) {
336 bytes[i] = (byte) (value >> shift);
339 setBytes(key, bytes);
343 * Returns a byte from a specific field
345 * @param key the name of the field
348 public byte getByte(String key) throws FieldException {
349 return definition.getField(key).getByte(data);
353 * Returns the insteon address from a specific field
355 * @param key the name of the field
356 * @return the insteon address
358 public InsteonAddress getInsteonAddress(String key) throws FieldException {
359 return definition.getField(key).getAddress(data);
363 * Returns the x10 address
365 * @return the x10 address
367 public @Nullable X10Address getX10Address() throws FieldException {
368 return isX10Address() ? new X10Address(getByte("rawX10")) : null;
372 * Returns a byte array starting from a specific field
374 * @param key the name of the first field
375 * @param numBytes number of bytes to get
376 * @return the byte array
378 public byte[] getBytes(String key, int numBytes) throws FieldException {
379 int offset = definition.getField(key).getOffset();
380 if (offset < 0 || offset + numBytes > data.length) {
381 throw new FieldException("data index out of bounds!");
383 return Arrays.copyOfRange(data, offset, offset + numBytes);
387 * Returns a byte array starting from a specific field as an up to 32-bit integer
389 * @param key the name of the first field
390 * @param numBytes number of bytes to use for conversion
391 * @return the integer
393 public int getInt(String key, int numBytes) throws FieldException {
394 if (numBytes < 1 || numBytes > 4) {
395 throw new FieldException("number of bytes out of bounds!");
398 int shift = 8 * (numBytes - 1);
399 for (byte b : getBytes(key, numBytes)) {
400 i |= (b & 0xFF) << shift;
407 * Returns a byte from a specific field as a 8-bit integer
409 * @param key the name of the field
410 * @return the integer
412 public int getInt(String key) throws FieldException {
413 return getByte(key) & 0xFF;
417 * Returns a 2-byte array starting from a specific field as a 16-bit integer
419 * @param key the name of the first field
420 * @return the integer
422 public int getInt16(String key) throws FieldException {
423 return getInt(key, 2);
427 * Returns a 3-byte array starting from a specific field as a 24-bit integer
429 * @param key the name of the first field
430 * @return the integer
432 public int getInt24(String key) throws FieldException {
433 return getInt(key, 3);
437 * Returns a 4-byte array starting from a specific field as a 32-bit integer
439 * @param key the name of the first field
440 * @return the integer
442 public int getInt32(String key) throws FieldException {
443 return getInt(key, 4);
447 * Returns a byte as a hex string
449 * @param key the name of the field
450 * @return the hex string
452 public String getHexString(String key) throws FieldException {
453 return HexUtils.getHexString(getByte(key));
457 * Returns a byte array starting from a certain field as a hex string
459 * @param key the name of the field
460 * @param numBytes number of bytes to get
461 * @return the hex string
463 public String getHexString(String key, int numBytes) throws FieldException {
464 return HexUtils.getHexString(getBytes(key, numBytes), numBytes);
468 * Returns group based on specific message characteristics
470 * @return group number if available, otherwise -1
472 public int getGroup() {
474 if (isAllLinkBroadcast()) {
475 return getInsteonAddress("toAddress").getLowByte() & 0xFF;
477 if (isAllLinkCleanup()) {
478 return getInt("command2");
481 byte cmd1 = getByte("command1");
482 byte cmd2 = getByte("command2");
483 // group number for specific extended msg located in userData1 byte
484 if (cmd1 == 0x2E && cmd2 == 0x00) {
485 return getInt("userData1");
488 } catch (FieldException e) {
489 logger.warn("got field exception on msg: {}", e.getMessage());
495 * Returns msg type based on message flags
499 public MsgType getType() {
501 return MsgType.valueOf(getInt("messageFlags"));
502 } catch (FieldException | IllegalArgumentException e) {
503 return MsgType.INVALID;
508 * Sets the userData fields from a byte array
510 * @param args list of user data arguments
512 public void setUserData(byte[] args) {
514 for (int i = 0; i < 14; i++) {
515 setByte("userData" + (i + 1), args.length > i ? args[i] : (byte) 0x00);
517 } catch (FieldException e) {
518 logger.warn("got field exception on msg {}:", e.getMessage());
523 * Calculates the CRC using the older 1-byte method
525 * @return the calculated crc
526 * @throws FieldException
528 public int calculateCRC() throws FieldException {
530 byte[] bytes = getBytes("command1", 15); // skip userData14
531 for (byte b : bytes) {
534 return (~crc + 1) & 0xFF;
538 * Calculates the CRC using the newer 2-byte method
540 * @return the calculated crc
541 * @throws FieldException
543 public int calculateCRC2() throws FieldException {
545 byte[] bytes = getBytes("command1", 14); // skip userData13/14
546 for (int loop = 0; loop < bytes.length; loop++) {
547 int b = bytes[loop] & 0xFF;
548 for (int bit = 0; bit < 8; bit++) {
550 if ((crc & 0x8000) == 0) {
553 if ((crc & 0x4000) == 0) {
556 if ((crc & 0x1000) == 0) {
559 if ((crc & 0x0008) == 0) {
562 crc = (crc << 1) | fb;
570 * Checks if message has a valid CRC using the older 1-byte method
572 * @return true if valid
574 public boolean hasValidCRC() {
576 return getInt("userData14") == calculateCRC();
577 } catch (FieldException e) {
578 logger.warn("got field exception on msg {}:", e.getMessage());
584 * Checks if message has a valid CRC using the newer 2-byte method is valid
586 * @return true if valid
588 public boolean hasValidCRC2() {
590 return getInt16("userData13") == calculateCRC2();
591 } catch (FieldException e) {
592 logger.warn("got field exception on msg {}:", e.getMessage());
598 * Sets the calculated CRC using the older 1-byte method
600 public void setCRC() {
602 int crc = calculateCRC();
603 setByte("userData14", (byte) crc);
604 } catch (FieldException e) {
605 logger.warn("got field exception on msg {}:", e.getMessage());
610 * Sets the calculated CRC using the newer 2-byte method
612 public void setCRC2() {
614 int crc = calculateCRC2();
615 setByte("userData13", (byte) ((crc >> 8) & 0xFF));
616 setByte("userData14", (byte) (crc & 0xFF));
617 } catch (FieldException e) {
618 logger.warn("got field exception on msg {}:", e.getMessage());
623 public boolean equals(@Nullable Object obj) {
630 if (getClass() != obj.getClass()) {
633 Msg other = (Msg) obj;
634 return Arrays.equals(data, other.data);
638 public int hashCode() {
639 final int prime = 31;
641 result = prime * result + Arrays.hashCode(data);
646 public String toString() {
647 String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
648 for (Field field : definition.getFields()) {
649 if ("messageFlags".equals(field.getName())) {
650 s += field.toString(data) + "=" + getType() + ":" + getHopsLeft() + ":" + getMaxHops() + "|";
652 s += field.toString(data) + "|";
659 * Factory method to create Msg from raw byte stream received from the serial port.
661 * @param buf the raw received bytes
662 * @param msgLen length of received buffer
663 * @param isExtended whether it is an extended message or not
664 * @return message, or null if the Msg cannot be created
666 public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
667 if (buf.length < 2) {
671 .ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(buf[1], isExtended, Direction.FROM_MODEM))
672 .filter(template -> template.getLength() == msgLen).map(template -> new Msg(template, buf, msgLen))
677 * Factory method to determine the header length of a received message
679 * @param cmd the message command received
680 * @return the length of the header to expect
682 public static int getHeaderLength(byte cmd) {
683 return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, Direction.FROM_MODEM))
684 .map(Msg::getHeaderLength).orElse(-1);
688 * Factory method to determine the length of a received message
690 * @param cmd the message command received
691 * @param isExtended if is an extended message
692 * @return message length, or -1 if length cannot be determined
694 public static int getMessageLength(byte cmd, boolean isExtended) {
696 .ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, isExtended, Direction.FROM_MODEM))
697 .map(Msg::getLength).orElse(-1);
701 * Factory method to determine if a message is extended
703 * @param buf the received bytes
704 * @param len the number of bytes received so far
705 * @param headerLength the known length of the header
706 * @return true if it is definitely extended, false if cannot be
707 * determined or if it is a standard message
709 public static boolean isExtended(byte[] buf, int len, int headerLength) {
710 if (headerLength <= 2) {
712 } // extended messages are longer
713 if (len < headerLength) {
715 } // not enough data to tell if extended
716 byte flags = buf[headerLength - 1]; // last byte says flags
717 boolean isExtended = BinaryUtils.isBitSet(flags & 0xFF, 4);
722 * Factory method to create a message to send for a given cmd
724 * @param cmd the message cmd to create, as defined in the xml file
725 * @return the insteon message
726 * @throws InvalidMessageTypeException
728 public static Msg makeMessage(byte cmd) throws InvalidMessageTypeException {
729 return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, Direction.TO_MODEM))
730 .map(Msg::new).orElseThrow(() -> new InvalidMessageTypeException(
731 "unknown message command: " + HexUtils.getHexString(cmd)));
735 * Factory method to create an Insteon message to send for a given type
737 * @param type the message type to create, as defined in the xml file
738 * @return the insteon message
739 * @throws InvalidMessageTypeException
741 public static Msg makeMessage(String type) throws InvalidMessageTypeException {
742 return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(type)).map(Msg::new)
743 .orElseThrow(() -> new InvalidMessageTypeException("unknown message type: " + type));
747 * Factory method to create a broadcast message to send
749 * @param group the broadcast group to send the message to
750 * @param cmd1 the message command 1 field
751 * @param cmd2 the message command 2 field
752 * @return the broadcast message
753 * @throws FieldException
754 * @throws InvalidMessageTypeException
756 public static Msg makeBroadcastMessage(int group, byte cmd1, byte cmd2)
757 throws FieldException, InvalidMessageTypeException {
758 Msg msg = makeMessage("SendStandardMessage");
759 msg.setAddress("toAddress", new InsteonAddress((byte) 0, (byte) 0, (byte) (group & 0xFF)));
760 msg.setByte("messageFlags", (byte) 0xCF);
761 msg.setByte("command1", cmd1);
762 msg.setByte("command2", cmd2);
763 msg.setQuietTime(0L);
768 * Factory method to create a standard message to send
770 * @param address the address to send the message to
771 * @param cmd1 the message command 1 field
772 * @param cmd2 the message command 2 field
773 * @return the standard message
774 * @throws FieldException
775 * @throws InvalidMessageTypeException
777 public static Msg makeStandardMessage(InsteonAddress address, byte cmd1, byte cmd2)
778 throws FieldException, InvalidMessageTypeException {
779 return makeStandardMessage(address, (byte) 0x0F, cmd1, cmd2);
783 * Factory method to create a standard message to send
785 * @param address the address to send the message to
786 * @param flags the message flags field
787 * @param cmd1 the message command 1 field
788 * @param cmd2 the message command 2 field
789 * @return the standard message
790 * @throws FieldException
791 * @throws InvalidMessageTypeException
793 public static Msg makeStandardMessage(InsteonAddress address, byte flags, byte cmd1, byte cmd2)
794 throws FieldException, InvalidMessageTypeException {
795 Msg msg = makeMessage("SendStandardMessage");
796 msg.setAddress("toAddress", address);
797 msg.setByte("messageFlags", flags);
798 msg.setByte("command1", cmd1);
799 msg.setByte("command2", cmd2);
800 // set default quiet time accounting for ack response
801 msg.setQuietTime(1000L);
806 * Factory method to create an extended message to send with optional CRC
808 * @param address the address to send the message to
809 * @param cmd1 the message command 1 field
810 * @param cmd2 the message command 2 field
811 * @param setCRC if the CRC should be set
812 * @return extended message
813 * @throws FieldException
814 * @throws InvalidMessageTypeException
816 public static Msg makeExtendedMessage(InsteonAddress address, byte cmd1, byte cmd2, boolean setCRC)
817 throws FieldException, InvalidMessageTypeException {
818 return makeExtendedMessage(address, cmd1, cmd2, new byte[] {}, setCRC);
822 * Factory method to create an extended message to send with specific user data and optional CRC
824 * @param address the address to send the message to
825 * @param cmd1 the message command 1 field
826 * @param cmd2 the message command 2 field
827 * @param data the message user data fields
828 * @param setCRC if the CRC should be set
829 * @return extended message
830 * @throws FieldException
831 * @throws InvalidMessageTypeException
833 public static Msg makeExtendedMessage(InsteonAddress address, byte cmd1, byte cmd2, byte[] data, boolean setCRC)
834 throws FieldException, InvalidMessageTypeException {
835 return makeExtendedMessage(address, (byte) 0x1F, cmd1, cmd2, data, setCRC);
839 * Factory method to create an extended message to send with specific user data and optional CRC
841 * @param address the address to send the message to
842 * @param flags the message flags field
843 * @param cmd1 the message command 1 field
844 * @param cmd2 the message command 2 field
845 * @param data the message user data fields
846 * @param setCRC if the CRC should be set
847 * @return extended message
848 * @throws FieldException
849 * @throws InvalidMessageTypeException
851 public static Msg makeExtendedMessage(InsteonAddress address, byte flags, byte cmd1, byte cmd2, byte[] data,
852 boolean setCRC) throws FieldException, InvalidMessageTypeException {
853 Msg msg = makeMessage("SendExtendedMessage");
854 msg.setAddress("toAddress", address);
855 msg.setByte("messageFlags", (byte) (flags | 0x10));
856 msg.setByte("command1", cmd1);
857 msg.setByte("command2", cmd2);
858 msg.setUserData(data);
862 // set default quiet time accounting for ack followed by direct response messages
863 msg.setQuietTime(2000L);
868 * Factory method to create an extended message to send with specific user data and CRC2
870 * @param address the address to send the message to
871 * @param cmd1 the message command 1 field
872 * @param cmd2 the message command 2 field
873 * @param data the message user data fields
874 * @return extended message
875 * @throws FieldException
876 * @throws InvalidMessageTypeException
878 public static Msg makeExtendedMessageCRC2(InsteonAddress address, byte cmd1, byte cmd2, byte[] data)
879 throws FieldException, InvalidMessageTypeException {
880 Msg msg = Msg.makeExtendedMessage(address, cmd1, cmd2, data, false);
886 * Factory method to create an X10 message to send
888 * @param cmd the X10 command
889 * @param flag the X10 flag
890 * @return the X10 message
891 * @throws FieldException
892 * @throws InvalidMessageTypeException
894 public static Msg makeX10Message(byte cmd, byte flag) throws FieldException, InvalidMessageTypeException {
895 Msg msg = makeMessage("SendX10Message");
896 msg.setByte("rawX10", cmd);
897 msg.setByte("X10Flag", flag);
898 msg.setQuietTime(300L);
903 * Factory method to create an X10 address message to send
905 * @param address the X10 address
906 * @return the X10 address message
907 * @throws FieldException
908 * @throws InvalidMessageTypeException
910 public static Msg makeX10AddressMessage(X10Address address) throws FieldException, InvalidMessageTypeException {
911 return makeX10Message(address.getCode(), X10Flag.ADDRESS.code());
915 * Factory method to create an X10 command message to send
917 * @param cmd the X10 command
918 * @return the X10 command message
919 * @throws FieldException
920 * @throws InvalidMessageTypeException
922 public static Msg makeX10CommandMessage(byte cmd) throws FieldException, InvalidMessageTypeException {
923 return makeX10Message(cmd, X10Flag.COMMAND.code());