[insteon] Refactor msg definition/factory and product data classes (#17537)

Signed-off-by: jsetton <jeremy.setton@gmail.com>
This commit is contained in:
Jeremy 2024-10-10 15:15:31 -04:00 committed by GitHub
parent 834b47c700
commit 7a5ba4e9b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 298 additions and 363 deletions

View File

@ -33,10 +33,10 @@ import org.openhab.binding.insteon.internal.device.InsteonScene;
import org.openhab.binding.insteon.internal.device.X10Address;
import org.openhab.binding.insteon.internal.device.X10Device;
import org.openhab.binding.insteon.internal.transport.PortListener;
import org.openhab.binding.insteon.internal.transport.message.Direction;
import org.openhab.binding.insteon.internal.transport.message.FieldException;
import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.transport.message.Msg;
import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
import org.openhab.binding.insteon.internal.transport.message.MsgDefinitionRegistry;
import org.openhab.binding.insteon.internal.utils.HexUtils;
import org.openhab.core.io.console.Console;

View File

@ -245,7 +245,7 @@ public class InsteonModem extends BaseDevice<InsteonAddress, InsteonBridgeHandle
int deviceCategory = msg.getInt("DeviceCategory");
int subCategory = msg.getInt("DeviceSubCategory");
ProductData productData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
ProductData productData = ProductData.makeInsteonProduct(deviceCategory, subCategory);
productData.setFirmwareVersion(msg.getInt("FirmwareVersion"));
DeviceType deviceType = productData.getDeviceType();

View File

@ -87,9 +87,9 @@ public class InsteonScene implements Scene {
}
public State getState() {
return getEntries().stream().allMatch(entry -> entry.getState() == UnDefType.NULL) ? UnDefType.NULL
: OnOffType.from(getEntries().stream().filter(entry -> entry.getState() != UnDefType.NULL)
.allMatch(entry -> entry.getState().equals(entry.getOnState())));
return getEntries().stream().noneMatch(SceneEntry::isStateDefined) ? UnDefType.NULL
: OnOffType
.from(getEntries().stream().filter(SceneEntry::isStateDefined).allMatch(SceneEntry::isStateOn));
}
public boolean hasEntry(InsteonAddress address) {
@ -354,6 +354,14 @@ public class InsteonScene implements Scene {
return state;
}
public boolean isStateDefined() {
return !UnDefType.NULL.equals(state);
}
public boolean isStateOn() {
return getOnState().equals(state);
}
public void setState(State state) {
this.state = state;
}

View File

@ -231,21 +231,9 @@ public class ProductData {
ProductData productData = new ProductData();
productData.setDeviceCategory(deviceCategory);
productData.setSubCategory(subCategory);
return productData;
}
/**
* Factory method for creating a ProductData for an Insteon product
*
* @param deviceCategory the Insteon device category
* @param subCategory the Insteon device subcategory
* @param srcData the source product data to use
* @return the product data
*/
public static ProductData makeInsteonProduct(int deviceCategory, int subCategory, @Nullable ProductData srcData) {
ProductData productData = makeInsteonProduct(deviceCategory, subCategory);
if (srcData != null) {
productData.update(srcData);
ProductData resourceData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
if (resourceData != null) {
productData.update(resourceData);
}
return productData;
}

View File

@ -45,9 +45,9 @@ public class ProductDataRegistry extends InsteonResourceLoader {
*
* @param deviceCategory device category to match
* @param subCategory device subcategory to match
* @return product data matching provided parameters
* @return product data if matching provided parameters, otherwise null
*/
public ProductData getProductData(int deviceCategory, int subCategory) {
public @Nullable ProductData getProductData(int deviceCategory, int subCategory) {
int productId = getProductId(deviceCategory, subCategory);
if (!products.containsKey(productId)) {
logger.warn("unknown product for devCat:{} subCat:{} in device products xml file",
@ -55,19 +55,7 @@ public class ProductDataRegistry extends InsteonResourceLoader {
// fallback to matching product id using device category only
productId = getProductId(deviceCategory, ProductData.SUB_CATEGORY_UNKNOWN);
}
return ProductData.makeInsteonProduct(deviceCategory, subCategory, products.get(productId));
}
/**
* Returns the device type for a given dev/sub category
*
* @param deviceCategory device category to match
* @param subCategory device subcategory to match
* @return device type matching provided parameters
*/
public @Nullable DeviceType getDeviceType(int deviceCategory, int subCategory) {
return getProductData(deviceCategory, subCategory).getDeviceType();
return products.get(productId);
}
/**
@ -143,7 +131,9 @@ public class ProductDataRegistry extends InsteonResourceLoader {
logger.warn("overwriting previous definition of product {}", products.get(productId));
}
ProductData productData = ProductData.makeInsteonProduct(deviceCategory, subCategory);
ProductData productData = new ProductData();
productData.setDeviceCategory(deviceCategory);
productData.setSubCategory(subCategory);
productData.setProductKey(productKey);
productData.setFirstRecordLocation(firstRecord);

View File

@ -17,6 +17,7 @@ import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -64,6 +65,8 @@ public enum RampRate {
SEC_0_2(0x1E, 0.2),
INSTANT(0x1F, 0.1);
private static final List<String> SUPPORTED_FEATURE_TYPES = List.of(FEATURE_TYPE_GENERIC_DIMMER);
private static final Map<Integer, RampRate> VALUE_MAP = Arrays.stream(values())
.collect(Collectors.toUnmodifiableMap(rate -> rate.value, Function.identity()));
@ -105,7 +108,7 @@ public enum RampRate {
* @return true if supported
*/
public static boolean supportsFeatureType(String featureType) {
return FEATURE_TYPE_GENERIC_DIMMER.equals(featureType);
return SUPPORTED_FEATURE_TYPES.contains(featureType);
}
/**

View File

@ -24,7 +24,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonModem;
import org.openhab.binding.insteon.internal.device.ProductData;
import org.openhab.binding.insteon.internal.device.ProductDataRegistry;
import org.openhab.binding.insteon.internal.transport.PortListener;
import org.openhab.binding.insteon.internal.transport.message.FieldException;
import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
@ -290,7 +289,7 @@ public class ModemDBReader implements PortListener {
int subCategory = Byte.toUnsignedInt(toAddr.getMiddleByte());
int firmware = Byte.toUnsignedInt(toAddr.getLowByte());
int hardware = msg.getInt("command2");
ProductData productData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
ProductData productData = ProductData.makeInsteonProduct(deviceCategory, subCategory);
productData.setFirmwareVersion(firmware);
productData.setHardwareVersion(hardware);
// set product data if in modem db

View File

@ -62,8 +62,7 @@ public class InsteonDiscoveryService extends AbstractDiscoveryService {
public void discoverInsteonDevice(InsteonAddress address, @Nullable ProductData productData) {
InsteonModem modem = handler.getModem();
if (handler.isDeviceDiscoveryEnabled() && modem != null && modem.getDB().hasEntry(address)
&& !modem.hasDevice(address)) {
if (modem != null && modem.getDB().hasEntry(address) && !modem.hasDevice(address)) {
addInsteonDevice(address, productData);
} else {
removeInsteonDevice(address);
@ -72,8 +71,7 @@ public class InsteonDiscoveryService extends AbstractDiscoveryService {
public void discoverInsteonScene(int group) {
InsteonModem modem = handler.getModem();
if (handler.isSceneDiscoveryEnabled() && modem != null && modem.getDB().hasBroadcastGroup(group)
&& !modem.hasScene(group)) {
if (modem != null && modem.getDB().hasBroadcastGroup(group) && !modem.hasScene(group)) {
addInsteonScene(group);
} else {
removeInsteonScene(group);

View File

@ -284,14 +284,14 @@ public class InsteonBridgeHandler extends InsteonBaseThingHandler implements Bri
protected void discoverInsteonDevice(InsteonAddress address, @Nullable ProductData productData) {
InsteonDiscoveryService discoveryService = getDiscoveryService();
if (discoveryService != null) {
if (discoveryService != null && isDeviceDiscoveryEnabled()) {
scheduler.execute(() -> discoveryService.discoverInsteonDevice(address, productData));
}
}
protected void discoverInsteonScene(int group) {
InsteonDiscoveryService discoveryService = getDiscoveryService();
if (discoveryService != null) {
if (discoveryService != null && isSceneDiscoveryEnabled()) {
scheduler.execute(() -> discoveryService.discoverInsteonScene(group));
}
}

View File

@ -18,6 +18,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Defines the data types that can be used in the fields of a message.
@ -29,8 +30,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public enum DataType {
BYTE("byte", 1),
ADDRESS("address", 3),
INVALID("invalid", -1);
ADDRESS("address", 3);
private static final Map<String, DataType> NAME_MAP = Arrays.stream(values())
.collect(Collectors.toUnmodifiableMap(type -> type.name, Function.identity()));
@ -55,9 +55,9 @@ public enum DataType {
* Factory method for getting a DataType from the data type name
*
* @param name the data type name
* @return the data type
* @return the data type if defined, otherwise null
*/
public static DataType get(String name) {
return NAME_MAP.getOrDefault(name, DataType.INVALID);
public static @Nullable DataType get(String name) {
return NAME_MAP.get(name);
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.transport.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Direction} represents a message direction
*
* @author Jeremy Setton - Initial contribution
*/
@NonNullByDefault
public enum Direction {
FROM_MODEM("IN"),
TO_MODEM("OUT");
private final String label;
private Direction(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
public static @Nullable Direction get(String value) {
try {
return Direction.valueOf(value);
} catch (IllegalArgumentException e) {
return null;
}
}
}

View File

@ -34,6 +34,12 @@ public final class Field {
private final int offset;
private final DataType type;
public Field(String name, int offset, DataType type) {
this.name = name;
this.offset = offset;
this.type = type;
}
public String getName() {
return name;
}
@ -42,16 +48,6 @@ public final class Field {
return offset;
}
public DataType getType() {
return type;
}
public Field(String name, DataType type, int offset) {
this.name = name;
this.type = type;
this.offset = offset;
}
private void check(int len, DataType t) throws FieldException {
if (offset + type.getSize() > len) {
throw new FieldException("field write beyond end of msg");
@ -61,16 +57,16 @@ public final class Field {
}
}
public void set(byte[] data, Object o) throws FieldException {
public void set(byte[] data, String value) throws FieldException, IllegalArgumentException {
switch (type) {
case BYTE:
setByte(data, (Byte) o);
byte b = value.isEmpty() ? 0x00 : (byte) HexUtils.toInteger(value);
setByte(data, b);
break;
case ADDRESS:
setAddress(data, (InsteonAddress) o);
InsteonAddress address = value.isEmpty() ? InsteonAddress.UNKNOWN : new InsteonAddress(value);
setAddress(data, address);
break;
default:
throw new FieldException("field data type unknown");
}
}
@ -141,8 +137,6 @@ public final class Field {
case ADDRESS:
s += getAddress(data).toString();
break;
default:
throw new FieldException("field data type unknown");
}
} catch (FieldException e) {
s += "NULL";

View File

@ -38,38 +38,21 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class Msg {
public static enum Direction {
TO_MODEM,
FROM_MODEM
}
private final Logger logger = LoggerFactory.getLogger(Msg.class);
private byte[] data;
private int headerLength;
private Direction direction;
private MsgDefinition definition = new MsgDefinition();
private final byte[] data;
private final MsgDefinition definition;
private long quietTime = 0;
private boolean replayed = false;
private long timestamp = System.currentTimeMillis();
public Msg(int headerLength, int dataLength, Direction direction) {
this.data = new byte[dataLength];
this.headerLength = headerLength;
this.direction = direction;
public Msg(byte[] data, MsgDefinition definition) {
this.data = Arrays.copyOf(data, definition.getLength());
this.definition = definition;
}
public Msg(Msg msg, byte[] data, int dataLength) {
this.data = Arrays.copyOf(data, dataLength);
this.headerLength = msg.headerLength;
this.direction = msg.direction;
// the message definition usually doesn't change, but just to be sure...
this.definition = new MsgDefinition(msg.definition);
}
public Msg(Msg msg) {
this(msg, msg.data, msg.data.length);
public Msg(MsgDefinition definition) {
this(definition.getData(), definition);
}
public byte[] getData() {
@ -81,15 +64,11 @@ public class Msg {
}
public int getHeaderLength() {
return headerLength;
return definition.getHeaderLength();
}
public Direction getDirection() {
return direction;
}
public MsgDefinition getDefinition() {
return definition;
return definition.getDirection();
}
public long getQuietTime() {
@ -129,11 +108,11 @@ public class Msg {
}
public boolean isInbound() {
return direction == Direction.FROM_MODEM;
return getDirection() == Direction.FROM_MODEM;
}
public boolean isOutbound() {
return direction == Direction.TO_MODEM;
return getDirection() == Direction.TO_MODEM;
}
public boolean isEcho() {
@ -244,10 +223,6 @@ public class Msg {
return replayed;
}
public void setDefinition(MsgDefinition definition) {
this.definition = definition;
}
public void setQuietTime(long quietTime) {
this.quietTime = quietTime;
}
@ -256,10 +231,6 @@ public class Msg {
this.replayed = replayed;
}
public void addField(Field f) {
definition.addField(f);
}
public boolean containsField(String key) {
return definition.containsField(key);
}
@ -283,12 +254,11 @@ public class Msg {
/**
* Sets a byte at a specific field
*
* @param key the string key in the message definition
* @param key the name of the field
* @param value the byte to put
*/
public void setByte(String key, byte value) throws FieldException {
Field field = definition.getField(key);
field.setByte(data, value);
definition.getField(key).setByte(data, value);
}
/**
@ -443,27 +413,6 @@ public class Msg {
return getInt(key, 4);
}
/**
* Returns a byte as a hex string
*
* @param key the name of the field
* @return the hex string
*/
public String getHexString(String key) throws FieldException {
return HexUtils.getHexString(getByte(key));
}
/**
* Returns a byte array starting from a certain field as a hex string
*
* @param key the name of the field
* @param numBytes number of bytes to get
* @return the hex string
*/
public String getHexString(String key, int numBytes) throws FieldException {
return HexUtils.getHexString(getBytes(key, numBytes), numBytes);
}
/**
* Returns group based on specific message characteristics
*
@ -644,7 +593,7 @@ public class Msg {
@Override
public String toString() {
String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
String s = definition.getDirection() + ":";
for (Field field : definition.getFields()) {
if ("messageFlags".equals(field.getName())) {
s += field.toString(data) + "=" + getType() + ":" + getHopsLeft() + ":" + getMaxHops() + "|";
@ -668,8 +617,8 @@ public class Msg {
return null;
}
return Optional
.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(buf[1], isExtended, Direction.FROM_MODEM))
.filter(template -> template.getLength() == msgLen).map(template -> new Msg(template, buf, msgLen))
.ofNullable(MsgDefinitionRegistry.getInstance().getDefinition(buf[1], isExtended, Direction.FROM_MODEM))
.filter(definition -> definition.getLength() == msgLen).map(definition -> new Msg(buf, definition))
.orElse(null);
}
@ -678,10 +627,12 @@ public class Msg {
*
* @param cmd the message command received
* @return the length of the header to expect
* @throws InvalidMessageTypeException
*/
public static int getHeaderLength(byte cmd) {
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, Direction.FROM_MODEM))
.map(Msg::getHeaderLength).orElse(-1);
public static int getHeaderLength(byte cmd) throws InvalidMessageTypeException {
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getDefinition(cmd, Direction.FROM_MODEM))
.map(MsgDefinition::getHeaderLength)
.orElseThrow(() -> new InvalidMessageTypeException("unknown message command"));
}
/**
@ -690,32 +641,27 @@ public class Msg {
* @param cmd the message command received
* @param isExtended if is an extended message
* @return message length, or -1 if length cannot be determined
* @throws InvalidMessageTypeException
*/
public static int getMessageLength(byte cmd, boolean isExtended) {
public static int getMessageLength(byte cmd, boolean isExtended) throws InvalidMessageTypeException {
return Optional
.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, isExtended, Direction.FROM_MODEM))
.map(Msg::getLength).orElse(-1);
.ofNullable(MsgDefinitionRegistry.getInstance().getDefinition(cmd, isExtended, Direction.FROM_MODEM))
.map(MsgDefinition::getLength)
.orElseThrow(() -> new InvalidMessageTypeException("unknown message command"));
}
/**
* Factory method to determine if a message is extended
*
* @param buf the received bytes
* @param len the number of bytes received so far
* @param headerLength the known length of the header
* @return true if it is definitely extended, false if cannot be
* determined or if it is a standard message
* @param headerLength the header length
* @return true if is extended
*/
public static boolean isExtended(byte[] buf, int len, int headerLength) {
if (headerLength <= 2) {
public static boolean isExtended(byte[] buf, int headerLength) {
if (headerLength <= 2 || buf.length < headerLength) {
return false;
} // extended messages are longer
if (len < headerLength) {
return false;
} // not enough data to tell if extended
byte flags = buf[headerLength - 1]; // last byte says flags
boolean isExtended = BinaryUtils.isBitSet(flags & 0xFF, 4);
return isExtended;
}
return BinaryUtils.isBitSet(buf[headerLength - 1] & 0xFF, 4);
}
/**
@ -726,7 +672,7 @@ public class Msg {
* @throws InvalidMessageTypeException
*/
public static Msg makeMessage(byte cmd) throws InvalidMessageTypeException {
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(cmd, Direction.TO_MODEM))
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getDefinition(cmd, Direction.TO_MODEM))
.map(Msg::new).orElseThrow(() -> new InvalidMessageTypeException(
"unknown message command: " + HexUtils.getHexString(cmd)));
}
@ -739,7 +685,7 @@ public class Msg {
* @throws InvalidMessageTypeException
*/
public static Msg makeMessage(String type) throws InvalidMessageTypeException {
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getTemplate(type)).map(Msg::new)
return Optional.ofNullable(MsgDefinitionRegistry.getInstance().getDefinition(type)).map(Msg::new)
.orElseThrow(() -> new InvalidMessageTypeException("unknown message type: " + type));
}

View File

@ -12,12 +12,11 @@
*/
package org.openhab.binding.insteon.internal.transport.message;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.insteon.internal.utils.BinaryUtils;
/**
* Definition (layout) of an Insteon message. Says which bytes go where.
@ -30,34 +29,34 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class MsgDefinition {
private Map<String, Field> fields = new HashMap<>();
private final byte[] data;
private final int headerLength;
private final Direction direction;
private final Map<String, Field> fields;
MsgDefinition() {
public MsgDefinition(byte[] data, int headerLength, Direction direction, Map<String, Field> fields) {
this.data = data;
this.headerLength = headerLength;
this.direction = direction;
this.fields = fields;
}
MsgDefinition(MsgDefinition definition) {
fields = new HashMap<>(definition.fields);
public byte[] getData() {
return data;
}
public List<Field> getFields() {
return fields.values().stream().sorted(Comparator.comparing(Field::getOffset)).toList();
public int getLength() {
return data.length;
}
public boolean containsField(String name) {
return fields.containsKey(name);
public int getHeaderLength() {
return headerLength;
}
public void addField(Field field) {
fields.put(field.getName(), field);
public Direction getDirection() {
return direction;
}
/**
* Finds field of a given name
*
* @param name name of the field to search for
* @return reference to field
* @throws FieldException if no such field can be found
*/
public Field getField(String name) throws FieldException {
Field field = fields.get(name);
if (field == null) {
@ -65,4 +64,41 @@ public class MsgDefinition {
}
return field;
}
public List<Field> getFields() {
return fields.values().stream().toList();
}
public boolean containsField(String name) {
return fields.containsKey(name);
}
public byte getByte(String name) throws FieldException {
return getField(name).getByte(data);
}
public byte getCommand() {
try {
return getByte("Cmd");
} catch (FieldException e) {
return (byte) 0xFF;
}
}
public boolean isExtended() {
try {
return BinaryUtils.isBitSet(getByte("messageFlags"), 4);
} catch (FieldException e) {
return false;
}
}
@Override
public String toString() {
String s = direction + ":";
for (Field field : getFields()) {
s += field.toString(data) + "|";
}
return s;
}
}

View File

@ -14,14 +14,10 @@ package org.openhab.binding.insteon.internal.transport.message;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.InsteonResourceLoader;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
import org.openhab.binding.insteon.internal.utils.HexUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@ -39,44 +35,46 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
private static final MsgDefinitionRegistry MSG_DEFINITION_REGISTRY = new MsgDefinitionRegistry();
private static final String RESOURCE_NAME = "/msg-definitions.xml";
private Map<String, Msg> definitions = new LinkedHashMap<>();
private Map<String, MsgDefinition> definitions = new LinkedHashMap<>();
private MsgDefinitionRegistry() {
super(RESOURCE_NAME);
}
/**
* Returns message template for a given type
* Returns message definition for a given type
*
* @param type message type to match
* @return message template if found, otherwise null
* @return message definition if found, otherwise null
*/
public @Nullable Msg getTemplate(String type) {
public @Nullable MsgDefinition getDefinition(String type) {
return definitions.get(type);
}
/**
* Returns message template for a given command and direction
* Returns message definition for a given command and direction
*
* @param cmd message command to match
* @param direction message direction to match
* @return message template if found, otherwise null
* @return message definition if found, otherwise null
*/
public @Nullable Msg getTemplate(byte cmd, Direction direction) {
return getTemplate(cmd, null, direction);
public @Nullable MsgDefinition getDefinition(byte cmd, Direction direction) {
return getDefinition(cmd, null, direction);
}
/**
* Returns message template for a given command, extended flag and direction
* Returns message definition for a given command, extended flag and direction
*
* @param cmd message command to match
* @param isExtended if message is extended
* @param direction message direction to match
* @return message template if found, otherwise null
* @return message definition if found, otherwise null
*/
public @Nullable Msg getTemplate(byte cmd, @Nullable Boolean isExtended, Direction direction) {
return definitions.values().stream().filter(msg -> msg.getCommand() == cmd && msg.getDirection() == direction
&& (isExtended == null || msg.isExtended() == isExtended)).findFirst().orElse(null);
public @Nullable MsgDefinition getDefinition(byte cmd, @Nullable Boolean isExtended, Direction direction) {
return definitions.values().stream()
.filter(definition -> definition.getCommand() == cmd && definition.getDirection() == direction
&& (isExtended == null || definition.isExtended() == isExtended))
.findFirst().orElse(null);
}
/**
@ -84,7 +82,7 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
*
* @return currently known message definitions
*/
public Map<String, Msg> getDefinitions() {
public Map<String, MsgDefinition> getDefinitions() {
return definitions;
}
@ -131,12 +129,22 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
* @throws SAXException
*/
private void parseMsgDefinition(Element element) throws SAXException {
LinkedHashMap<Field, Object> fields = new LinkedHashMap<>();
String name = element.getAttribute("name");
Direction direction = Direction.valueOf(element.getAttribute("direction"));
int length = element.hasAttribute("length") ? Integer.parseInt(element.getAttribute("length")) : 0;
if (name.isEmpty()) {
throw new SAXException("undefined message definition name");
}
Direction direction = Direction.get(element.getAttribute("direction"));
if (direction == null) {
throw new SAXException("invalid direction for message definition " + name);
}
int length = getAttributeAsInteger(element, "length", 0);
if (length == 0) {
throw new SAXException("undefined length for message definition " + name);
}
int headerLength = 0;
int offset = 0;
byte[] data = new byte[length];
Map<String, Field> fields = new LinkedHashMap<>();
NodeList nodes = element.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
@ -144,27 +152,26 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element child = (Element) node;
String nodeName = child.getNodeName();
if ("header".equals(nodeName)) {
headerLength = parseHeader(child, fields);
// Increment the offset by the header length
offset += headerLength;
} else {
if (!"header".equals(nodeName)) {
// Increment the offset by the field data type length
offset += parseField(child, offset, fields);
offset += parseField(child, offset, data, fields);
} else if (offset == 0) {
headerLength = parseHeader(child, data, fields);
// Set the offset to the header length
offset = headerLength;
}
}
}
if (length == 0) {
length = offset;
} else if (offset != length) {
throw new SAXException("actual msg length " + offset + " differs from given msg length " + length);
if (headerLength == 0) {
throw new SAXException("undefined header for message definition " + name);
}
if (offset != length) {
throw new SAXException("actual msg length " + offset + " differs from given length " + length);
}
try {
Msg msg = makeMsgTemplate(fields, headerLength, length, direction);
definitions.put(name, msg);
} catch (FieldException e) {
throw new SAXException("failed to create message definition " + name + ":", e);
MsgDefinition definition = new MsgDefinition(data, headerLength, direction, fields);
if (definitions.put(name, definition) != null) {
logger.warn("duplicate message definition {}", name);
}
}
@ -172,12 +179,16 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
* Parses header node
*
* @param element element to parse
* @param data msg data to update
* @param fields fields map to update
* @return header length
* @throws SAXException
*/
private int parseHeader(Element element, LinkedHashMap<Field, Object> fields) throws SAXException {
int length = Integer.parseInt(element.getAttribute("length"));
private int parseHeader(Element element, byte[] data, Map<String, Field> fields) throws SAXException {
int length = getAttributeAsInteger(element, "length", 0);
if (length == 0) {
throw new SAXException("undefined header length");
}
int offset = 0;
NodeList nodes = element.getChildNodes();
@ -186,10 +197,10 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element child = (Element) node;
// Increment the offset by the field data type length
offset += parseField(child, offset, fields);
offset += parseField(child, offset, data, fields);
}
}
if (length != offset) {
if (offset != length) {
throw new SAXException("actual header length " + offset + " differs from given length " + length);
}
return length;
@ -200,95 +211,29 @@ public class MsgDefinitionRegistry extends InsteonResourceLoader {
*
* @param element element to parse
* @param offset msg offset
* @param data msg data to update
* @param fields fields map to update
* @return field data type length
* @throws SAXException
*/
private int parseField(Element element, int offset, LinkedHashMap<Field, Object> fields) throws SAXException {
private int parseField(Element element, int offset, byte[] data, Map<String, Field> fields) throws SAXException {
String name = element.getAttribute("name");
if (name == null) {
throw new SAXException("undefined field name");
}
DataType dataType = DataType.get(element.getNodeName());
Field field = new Field(name, dataType, offset);
Object value = getFieldValue(dataType, element.getTextContent().trim());
fields.put(field, value);
if (dataType == null) {
throw new SAXException("invalid field data type");
}
Field field = new Field(name, offset, dataType);
try {
field.set(data, element.getTextContent());
} catch (FieldException | IllegalArgumentException e) {
throw new SAXException("failed to set field data:", e);
}
if (!name.isEmpty()) {
fields.put(name, field);
}
return dataType.getSize();
}
/**
* Returns field value
*
* @param dataType field data type
* @param value value to convert
* @return field value
* @throws SAXException
*/
private Object getFieldValue(DataType dataType, String value) throws SAXException {
switch (dataType) {
case BYTE:
return getByteValue(value);
case ADDRESS:
return getAddressValue(value);
default:
throw new SAXException("invalid field data type");
}
}
/**
* Returns field value as a byte
*
* @param value value to convert
* @return byte
* @throws SAXException
*/
private byte getByteValue(String value) throws SAXException {
try {
return value.isEmpty() ? 0x00 : (byte) HexUtils.toInteger(value);
} catch (NumberFormatException e) {
throw new SAXException("invalid field byte value: " + value);
}
}
/**
* Returns field value as an insteon address
*
* @param value value to convert
* @return insteon address
* @throws SAXException
*/
private InsteonAddress getAddressValue(String value) throws SAXException {
try {
return value.isEmpty() ? InsteonAddress.UNKNOWN : new InsteonAddress(value);
} catch (IllegalArgumentException e) {
throw new SAXException("invalid field address value: " + value);
}
}
/**
* Returns new message template
*
* @param fields msg fields
* @param length msg length
* @param headerLength header length
* @param direction msg direction
* @return new msg template
* @throws FieldException
*/
private Msg makeMsgTemplate(Map<Field, Object> fields, int headerLength, int length, Direction direction)
throws FieldException {
Msg msg = new Msg(headerLength, length, direction);
for (Entry<Field, Object> entry : fields.entrySet()) {
Field field = entry.getKey();
byte[] data = msg.getData();
field.set(data, entry.getValue());
if (!field.getName().isEmpty()) {
msg.addField(field);
}
}
return msg;
}
/**
* Singleton instance function
*

View File

@ -97,8 +97,7 @@ public class MsgFactory {
logger.trace("got pure nack!");
removeFromBuffer(1);
try {
msg = Msg.makeMessage("PureNACK");
return msg;
return Msg.makeMessage("PureNACK");
} catch (InvalidMessageTypeException e) {
return null;
}
@ -112,39 +111,23 @@ public class MsgFactory {
// If not, we return null, and expect this method to be called again
// when more data has come in.
if (end > 1) {
// we have some data, but do we have enough to read the entire header?
int headerLength = Msg.getHeaderLength(buf[1]);
boolean isExtended = Msg.isExtended(buf, end, headerLength);
logger.trace("header length expected: {} extended: {}", headerLength, isExtended);
if (headerLength < 0) {
try {
int headerLength = Msg.getHeaderLength(buf[1]);
logger.trace("header length expected: {}", headerLength);
if (end >= headerLength) {
boolean isExtended = Msg.isExtended(buf, headerLength);
int msgLen = Msg.getMessageLength(buf[1], isExtended);
logger.trace("msgLen expected: {} extended: {}", msgLen, isExtended);
if (end >= msgLen) {
msg = Msg.createMessage(buf, msgLen, isExtended);
removeFromBuffer(msgLen);
}
}
} catch (InvalidMessageTypeException e) {
if (logger.isDebugEnabled()) {
logger.debug("got unknown command code: {}", HexUtils.getHexString(buf[1]));
}
removeFromBuffer(1); // get rid of the leading 0x02 so draining works
bail();
} else if (headerLength >= 2) {
if (end >= headerLength) {
// only when the header is complete do we know that isExtended is correct!
int msgLen = Msg.getMessageLength(buf[1], isExtended);
logger.trace("msgLen expected: {}", msgLen);
if (msgLen < 0) {
// Cannot make sense out of the combined command code & isExtended flag.
if (logger.isDebugEnabled()) {
logger.debug("got unknown command code/ext flag: {}", HexUtils.getHexString(buf[1]));
}
removeFromBuffer(1);
bail();
} else if (msgLen > 0) {
if (end >= msgLen) {
msg = Msg.createMessage(buf, msgLen, isExtended);
removeFromBuffer(msgLen);
}
} else { // should never happen
logger.warn("invalid message length, internal error!");
}
}
} else { // should never happen
logger.warn("invalid header length, internal error!");
}
}
// indicate no more messages available in buffer if empty or undefined message
@ -159,14 +142,11 @@ public class MsgFactory {
}
private void bail() throws IOException {
drainBuffer(); // this will drain until end or it finds the next message start
throw new IOException("bad data received");
}
private void drainBuffer() {
while (end > 0 && buf[0] != 0x02) {
// drain buffer until end or the next message start
do {
removeFromBuffer(1);
}
} while (end > 0 && buf[0] != 0x02);
throw new IOException("bad data received");
}
private void removeFromBuffer(int len) {
@ -174,7 +154,9 @@ public class MsgFactory {
if (l > end) {
l = end;
}
System.arraycopy(buf, l, buf, 0, end + 1 - l);
end -= l;
if (l > 0) {
System.arraycopy(buf, l, buf, 0, end + 1 - l);
end -= l;
}
}
}

View File

@ -163,7 +163,7 @@
</header>
<byte name="ALLLinkGroup"/>
<byte name="ALLLinkCommand"/>
<byte name="ALLLinkCommand2">0x00</byte>
<byte name="ALLLinkCommand2"/>
</msg>
<msg name="SendALLLinkCommandReply" length="6" direction="FROM_MODEM">
<header length="2">
@ -205,20 +205,20 @@
</header>
<byte name="command1"/>
<byte name="command2"/>
<byte name="userData1">0x00</byte>
<byte name="userData2">0x00</byte>
<byte name="userData3">0x00</byte>
<byte name="userData4">0x00</byte>
<byte name="userData5">0x00</byte>
<byte name="userData6">0x00</byte>
<byte name="userData7">0x00</byte>
<byte name="userData8">0x00</byte>
<byte name="userData9">0x00</byte>
<byte name="userData10">0x00</byte>
<byte name="userData11">0x00</byte>
<byte name="userData12">0x00</byte>
<byte name="userData13">0x00</byte>
<byte name="userData14">0x00</byte>
<byte name="userData1"/>
<byte name="userData2"/>
<byte name="userData3"/>
<byte name="userData4"/>
<byte name="userData5"/>
<byte name="userData6"/>
<byte name="userData7"/>
<byte name="userData8"/>
<byte name="userData9"/>
<byte name="userData10"/>
<byte name="userData11"/>
<byte name="userData12"/>
<byte name="userData13"/>
<byte name="userData14"/>
</msg>
<msg name="SendExtendedMessageReply" length="23" direction="FROM_MODEM">
<header length="6">
@ -250,16 +250,16 @@
<byte>0x02</byte>
<byte name="Cmd">0x63</byte>
</header>
<byte name="rawX10"></byte>
<byte name="X10Flag">0x00</byte>
<byte name="rawX10"/>
<byte name="X10Flag"/>
</msg>
<msg name="SendX10MessageReply" length="5" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x63</byte>
</header>
<byte name="rawX10"></byte>
<byte name="X10Flag">0x00</byte>
<byte name="rawX10"/>
<byte name="X10Flag"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="StartALLLinking" length="4" direction="TO_MODEM">
@ -267,16 +267,16 @@
<byte>0x02</byte>
<byte name="Cmd">0x64</byte>
</header>
<byte name="LinkCode"></byte>
<byte name="ALLLinkGroup">0x00</byte>
<byte name="LinkCode"/>
<byte name="ALLLinkGroup"/>
</msg>
<msg name="StartALLLinkingReply" length="5" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x64</byte>
</header>
<byte name="LinkCode"></byte>
<byte name="ALLLinkGroup">0x00</byte>
<byte name="LinkCode"/>
<byte name="ALLLinkGroup"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="CancelALLLinking" length="2" direction="TO_MODEM">
@ -550,9 +550,9 @@
<byte name="RecordFlags"/>
<byte name="ALLLinkGroup"/>
<address name="LinkAddr"/>
<byte name="LinkData1">0x00</byte>
<byte name="LinkData2">0x00</byte>
<byte name="LinkData3">0x00</byte>
<byte name="LinkData1"/>
<byte name="LinkData2"/>
<byte name="LinkData3"/>
</msg>
<msg name="WriteDatabaseRecordReply" length="13" direction="FROM_MODEM">
<header length="2">
@ -603,9 +603,9 @@
<byte>0x02</byte>
<byte name="Cmd">0x79</byte>
</header>
<byte name="LinkData1">0x00</byte>
<byte name="LinkData2">0x00</byte>
<byte name="LinkData3">0x00</byte>
<byte name="LinkData1"/>
<byte name="LinkData2"/>
<byte name="LinkData3"/>
</msg>
<msg name="SetNextLinkDataReply" length="6" direction="FROM_MODEM">
<header length="2">