/** * Copyright (c) 2010-2017 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.modbus.internal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.openhab.binding.modbus.ModbusBindingProvider; import org.openhab.binding.modbus.internal.ItemIOConnection.IOType; import org.openhab.core.binding.BindingConfig; import org.openhab.core.items.Item; import org.openhab.core.library.items.ContactItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.openhab.model.item.binding.BindingConfigParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ModbusBindingConfig stores configuration of the item bound to Modbus * * @author dbkrasn * @since 1.1.0 */ public class ModbusBindingConfig implements BindingConfig { private static final Logger logger = LoggerFactory.getLogger(ModbusBindingConfig.class); private Class<? extends Item> itemClass = null; public Class<? extends Item> getItemClass() { return itemClass; } private String itemName = null; public String getItemName() { return itemName; } private List<ItemIOConnection> readConnections = new ArrayList<>(); private List<ItemIOConnection> writeConnections = new ArrayList<>(); private List<Class<? extends Command>> itemAcceptedCommandTypes; private List<Class<? extends State>> itemAcceptedDataTypes; private static final List<String> VALID_EXTENDED_ITEM_CONFIG_KEYS = Arrays .asList(new String[] { "type", "trigger", "transformation", "valueType" }); private static SimpleTokenizer keyValueTokenizer = new SimpleTokenizer('='); /** * Calculates new item state based on the new boolean value, current item state and item class * Used with item bound to "coil" type slaves * * Returns UnDefType.UNDEF for Number and other "uncompatible" item types * * @param previousState * @param b new boolean value * @param c class of the current item state * @param itemClass class of the item * * @return new item state */ protected State translateBoolean2State(State previousState, boolean b) { Class<? extends State> c = null; if (previousState == null) { c = UnDefType.class; } else { c = previousState.getClass(); } if (c == UnDefType.class && itemClass == SwitchItem.class) { return b ? OnOffType.ON : OnOffType.OFF; } else if (c == UnDefType.class && itemClass == ContactItem.class) { return b ? OpenClosedType.OPEN : OpenClosedType.CLOSED; } else if (c == OnOffType.class && itemClass == SwitchItem.class) { return b ? OnOffType.ON : OnOffType.OFF; } else if (c == OpenClosedType.class && itemClass == SwitchItem.class) { return b ? OnOffType.ON : OnOffType.OFF; } else if (c == OnOffType.class && itemClass == ContactItem.class) { return b ? OpenClosedType.OPEN : OpenClosedType.CLOSED; } else if (c == OpenClosedType.class && itemClass == ContactItem.class) { return b ? OpenClosedType.OPEN : OpenClosedType.CLOSED; } else { // Number items return UnDefType.UNDEF; } } /** * Constructor for config string * * Following simple formats are supported * * - slaveName:index * - slaveName:<readIndex:>writeIndex * * * In addition, extended format allows user to configure some additional details (inspired by http binding * configuration format): * - <[slaveName:readIndex:trigger=TRIGGER,transformation=TRANSFORMATION,valueType=VALUETYPE],>[slaveName: * readIndex:trigger=TRIGGER,transformation=TRANSFORMATION,valueType= * VALUETYPE] * * where one defines zero or more read/inbound entries (<) and zero or more write/outbound entries (>). These * entries * are separated by comma. Only * slaveName, index are mandatory * * (not supported) TYPE : command or state. On read entries tells whether state updates or commands are published. * On write entries * tells whether state updates or commands are listened. Specifying "default" is "state" with read entries, and * "command" with write entries. * TRIGGER : see javadoc on ItemIOConnection * TRANSFORMATION : see javadoc on ItemIOConnection * VALUETYPE : valuetype when encoding/decoding the modbus register data. Specifying "default" is slave's valuetype. * * * @param item * @param config * @param modbusGenericBindingProvider TODO * @throws BindingConfigParseException if */ public ModbusBindingConfig(Item item, String config) throws BindingConfigParseException { itemClass = item.getClass(); itemName = item.getName(); itemAcceptedCommandTypes = item.getAcceptedCommandTypes(); itemAcceptedDataTypes = item.getAcceptedDataTypes(); if (config.contains("[")) { logger.debug("Since '[' in item '{}' config string '{}', trying to parse it using extended parser", itemName, config); parseExtended(config); logger.debug("item '{}' parsed", itemName); } else { logger.debug("Since no '[' in item '{}' config string '{}', trying to parse it using simple syntax parser", itemName, config); parseSimple(config); logger.debug("item '{}' parsed", itemName); } } public List<ItemIOConnection> getReadConnectionsBySlaveName(String slave) { List<ItemIOConnection> connections = new ArrayList<>(); for (ItemIOConnection connection : getReadConnections()) { if (slave.equals(connection.getSlaveName())) { connections.add(connection); } } logger.trace("Item '{}' has the following read connections configured for slave {}: {}", itemName, slave, connections); return connections; } public List<ItemIOConnection> getWriteConnectionsByCommand(Command command) { List<ItemIOConnection> connections = new ArrayList<>(); for (ItemIOConnection connection : getWriteConnections()) { if (connection.supportsCommand(command)) { connections.add(connection); } } logger.trace("Item '{}' write connections for command {}: {}", itemName, command, connections); return connections; } private static String findValueMatchingKey(List<String> tokens, String key, String defaultValue) throws BindingConfigParseException { for (String token : tokens) { if (token.trim().isEmpty()) { // skip empty tokens (e.g. empty token in "key=val,,key2=val2") continue; } List<String> keyValue = keyValueTokenizer.parse(token); if (keyValue.get(0).trim().equals(key)) { return keyValue.get(1); } } return defaultValue; } private static void assertValidConfigurationKeys(List<String> tokens) throws BindingConfigParseException { for (String token : tokens) { if (token.trim().isEmpty()) { // skip empty tokens (e.g. empty token in "key=val,,key2=val2") continue; } List<String> keyValue = keyValueTokenizer.parse(token); if (keyValue.size() != 2) { throw new BindingConfigParseException(String.format("Invalid token '%s', expecting key=value", token)); } String key = keyValue.get(0).trim(); if (!VALID_EXTENDED_ITEM_CONFIG_KEYS.stream().anyMatch(validKey -> validKey.equals(key))) { throw new BindingConfigParseException( String.format("Unexpected token '%s, expecting key to be one of: %s", token, StringUtils.join(VALID_EXTENDED_ITEM_CONFIG_KEYS, ", "))); } } } private void parseExtended(String config) throws BindingConfigParseException { List<String> definitions = new SimpleTokenizer(']').parse(config); for (String origBindingDefinition : definitions) { String bindingDefinition = origBindingDefinition.trim(); if (bindingDefinition.isEmpty()) { // end of string continue; } else if (bindingDefinition.startsWith(",")) { bindingDefinition = bindingDefinition.substring(1).trim(); } try { String[] colonSplitted = bindingDefinition.split(Pattern.quote("["), 2)[1].split(":", 3); boolean read = bindingDefinition.charAt(0) == '<'; String slaveName = colonSplitted[0].trim(); int index; try { index = Integer.valueOf(colonSplitted[1].trim()); } catch (NumberFormatException e) { throw new BindingConfigParseException(String.format( "Could not parse '%s' as number. Please check item config syntax.", colonSplitted[1])); } List<String> tokens = colonSplitted.length > 2 ? new SimpleTokenizer(',').parse(colonSplitted[2].trim()) : Arrays.asList(); assertValidConfigurationKeys(tokens); IOType type; // Only "default" type supported. In the future we could add support to it a la mqtt (see constructor). String typeString = "default"; // findValueMatchingKey(tokens, "type", "default").trim().toUpperCase(); if ("default".equalsIgnoreCase(typeString)) { if (read) { type = IOType.STATE; } else { type = IOType.COMMAND; } } else { try { type = IOType.valueOf(typeString); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(String.format("Could not convert '%s' to one of %s", findValueMatchingKey(tokens, "type", "default"), Arrays.toString(IOType.values())), e); } } String trigger = findValueMatchingKey(tokens, "trigger", ItemIOConnection.TRIGGER_DEFAULT).trim(); String transformationString = StringEscapeUtils.unescapeJava( findValueMatchingKey(tokens, "transformation", Transformation.TRANSFORM_DEFAULT).trim()); Transformation transformation = new Transformation(transformationString); String valueType = findValueMatchingKey(tokens, "valueType", ItemIOConnection.VALUETYPE_DEFAULT); if (!"default".equalsIgnoreCase(valueType) && !Arrays.asList(ModbusBindingProvider.VALUE_TYPES).contains(valueType)) { throw new BindingConfigParseException( String.format("valuetype '%s' does not match expected: '%s or 'default'", valueType, String.join(", ", ModbusBindingProvider.VALUE_TYPES))); } ItemIOConnection connection = new ItemIOConnection(slaveName, index, type, trigger, transformation, valueType); if (read) { readConnections.add(connection); } else { writeConnections.add(connection); } logger.debug("Parsed IOConnection (read={}) for item '{}': {}", read, itemName, connection); } catch (Exception e) { String msg = String.format( "Parsing of item '%s' configuration '%s]' (as part of the whole config '%s') failed: %s %s", itemName, origBindingDefinition, config, e.getClass().getName(), e.getMessage()); logger.error(msg, e); BindingConfigParseException exception = new BindingConfigParseException(msg); exception.initCause(e); throw exception; } } } private void parseSimple(String config) throws BindingConfigParseException { int readIndex; int writeIndex; String slaveName; try { String[] items = config.split(":"); slaveName = items[0]; if (items.length == 2) { readIndex = Integer.valueOf(items[1]); writeIndex = Integer.valueOf(items[1]); } else if (items.length == 3) { validateSimpleIndexEntry(items[1], items[2]); if (items[1].charAt(0) == '<') { // items[1] is inbound (read from slave); items[2] is outbound (write to slave) readIndex = Integer.valueOf(items[1].substring(1, items[1].length())); writeIndex = Integer.valueOf(items[2].substring(1, items[2].length())); } else { readIndex = Integer.valueOf(items[2].substring(1, items[2].length())); writeIndex = Integer.valueOf(items[1].substring(1, items[1].length())); } } else { throw new BindingConfigParseException( String.format("Invalid number of registers in item '%s' configuration", itemName)); } } catch (BindingConfigParseException e) { throw e; } catch (Exception e) { BindingConfigParseException exception = new BindingConfigParseException( String.format("Item '%s' config ('%s') parsing failed: %s: %s", itemName, config, e.getClass().getName(), e.getMessage())); exception.initCause(e); throw exception; } readConnections.add(new ItemIOConnection(slaveName, readIndex, IOType.STATE)); writeConnections.add(new ItemIOConnection(slaveName, writeIndex, IOType.COMMAND)); } private static void validateSimpleIndexEntry(String entry1, String entry2) throws BindingConfigParseException { if (!((entry1.startsWith("<") && entry2.startsWith(">")) || (entry1.startsWith(">") && entry2.startsWith("<")))) { throw new BindingConfigParseException("Register references should be either :X or :<X:>Y"); } } public List<ItemIOConnection> getReadConnections() { return readConnections; } public List<ItemIOConnection> getWriteConnections() { return writeConnections; } public List<Class<? extends Command>> getItemAcceptedCommandTypes() { return itemAcceptedCommandTypes; } public List<Class<? extends State>> getItemAcceptedDataTypes() { return itemAcceptedDataTypes; } }