/** * Copyright (c) 2010-2016 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.knx.internal.config; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.NoSuchElementException; import org.openhab.binding.knx.config.KNXBindingProvider; import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper; import org.openhab.core.autoupdate.AutoUpdateBindingProvider; import org.openhab.core.binding.BindingConfig; import org.openhab.core.items.Item; import org.openhab.core.types.Type; import org.openhab.model.item.binding.AbstractGenericBindingProvider; import org.openhab.model.item.binding.BindingConfigParseException; import org.openhab.model.item.binding.BindingConfigReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.datapoint.CommandDP; import tuwien.auto.calimero.datapoint.Datapoint; import tuwien.auto.calimero.datapoint.DatapointMap; import tuwien.auto.calimero.datapoint.StateDP; import tuwien.auto.calimero.exception.KNXFormatException; /** * <p> * This class can parse information from the generic binding format and provides KNX binding information from it. It * registers as a {@link BindingConfigReader} service as well as as a {@link KNXBindingProvider} service. * </p> * * <p> * The syntax of the binding configuration strings accepted is the following: * <p> * <p> * <code> * knx="[<dptId>:][<]<mainGA>[[+<listeningGA>]+<listeningGA>..], * [<dptId>:][<]<mainGA>[[+<listeningGA>]+<listeningGA>..]" * </code> * </p> * where parts in brackets [] signify an optional information. * * <p> * Each comma-separated section corresponds to an KNX datapoint. There is usually one datapoint defined per accepted * command type of an openHAB item. If no datapoint type id is defined for the datapoint, this is automatically derived * from the list of accepted command types of the item - i.e. the second datapoint definition is mapped to the second * accepted command type of the item. * </p> * <p> * The optional '<' sign tells whether the datapoint accepts read requests on the KNX bus (it does, if the sign is * there) * </p> * * <p> * Here are some examples for valid binding configuration strings: * <ul> * <li>For an SwitchItem: * <ul> * <li><code>knx="1/1/10"</code></li> * <li><code>knx="1.001:1/1/10"</code></li> * <li><code>knx="<1/1/10"/code></li> * <li><code>knx="<1/1/10+0/1/13+0/1/14+0/1/15"</code></li> * </ul> * </li> * <li>For a RollershutterItem: * <ul> * <li><code>knx="4/2/10"</code></li> * <li><code>knx="4/2/10, 4/2/11"</code></li> * <li><code>knx="1.008:4/2/10, 5.006:4/2/11"</code></li> * <li><code>knx="<4/2/10+0/2/10, 5.006:4/2/11+0/2/11"</code></li> * </ul> * </li> * </ul> * * @author Kai Kreuzer * @since 0.3.0 * */ public class KNXGenericBindingProvider extends AbstractGenericBindingProvider implements KNXBindingProvider, AutoUpdateBindingProvider { /** the binding type to register for as a binding config reader */ public static final String KNX_BINDING_TYPE = "knx"; /** the suffix to mark a group address for start-stop-dimming */ private static final String START_STOP_MARKER_SUFFIX = "ss"; //Logger private static Logger logger = LoggerFactory.getLogger(KNXGenericBindingProvider.class); /** * {@inheritDoc} */ @Override public String getBindingType() { return KNX_BINDING_TYPE; } /** * @{inheritDoc} */ @Override public void validateItemType(Item item, String bindingConfig) throws BindingConfigParseException { // all types of items are valid ... } /** * {@inheritDoc} */ @Override public void processBindingConfiguration(String context, Item item, String bindingConfig) throws BindingConfigParseException { super.processBindingConfiguration(context, item, bindingConfig); addBindingConfig(item, parseBindingConfigString(item, bindingConfig)); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public Iterable<Datapoint> getDatapoints(final String itemName, final GroupAddress groupAddress) { synchronized (bindingConfigs) { try { Iterable<KNXBindingConfig> configList = Iterables.filter(Iterables.concat(bindingConfigs.values()), KNXBindingConfig.class); Iterable<KNXBindingConfigItem> configItemList = Iterables.filter(Iterables.concat(configList), KNXBindingConfigItem.class); Iterable<KNXBindingConfigItem> bindingConfigs = Iterables.filter(configItemList, new Predicate<KNXBindingConfigItem>() { @Override public boolean apply(KNXBindingConfigItem input) { return input.itemName.equals(itemName) && input.allDataPoints.contains(groupAddress); } }); Iterable<Datapoint> datapoints = Iterables.transform(bindingConfigs, new Function<KNXBindingConfigItem, Datapoint>() { @Override public Datapoint apply(KNXBindingConfigItem configItem) { return configItem.mainDataPoint; } }); return datapoints; } catch (NoSuchElementException e) { return null; } } } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public Iterable<Datapoint> getDatapoints(final String itemName, final Class<? extends Type> typeClass) { synchronized (bindingConfigs) { try { Iterable<KNXBindingConfig> configList = Iterables.filter(Iterables.concat(bindingConfigs.values()), KNXBindingConfig.class); Iterable<KNXBindingConfigItem> configItemList = Iterables.filter(Iterables.concat(configList), KNXBindingConfigItem.class); Iterable<KNXBindingConfigItem> bindingConfigs = Iterables.filter(configItemList, new Predicate<KNXBindingConfigItem>() { @Override public boolean apply(KNXBindingConfigItem input) { if (input == null) { return false; } if (input.itemName.equals(itemName)) { Class<?> dptTypeClass = KNXCoreTypeMapper.toTypeClass(input.mainDataPoint.getDPT()); return dptTypeClass != null && dptTypeClass.equals(typeClass); } return false; } }); Iterable<Datapoint> datapoints = Iterables.transform(bindingConfigs, new Function<KNXBindingConfigItem, Datapoint>() { @Override public Datapoint apply(KNXBindingConfigItem configItem) { return configItem.mainDataPoint; } }); return Lists.newArrayList(datapoints); } catch (NoSuchElementException e) { // ignore and return null return null; } } } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public Iterable<String> getListeningItemNames(final GroupAddress groupAddress) { synchronized (bindingConfigs) { Iterable<KNXBindingConfig> configList = Iterables.filter(Iterables.concat(bindingConfigs.values()), KNXBindingConfig.class); Iterable<KNXBindingConfigItem> configItemList = Iterables.filter(Iterables.concat(configList), KNXBindingConfigItem.class); Iterable<KNXBindingConfigItem> filteredBindingConfigs = Iterables.filter(configItemList, new Predicate<KNXBindingConfigItem>() { @Override public boolean apply(KNXBindingConfigItem input) { if (input == null) { return false; } return input.allDataPoints.contains(groupAddress); } }); return Iterables.transform(filteredBindingConfigs, new Function<KNXBindingConfigItem, String>() { @Override public String apply(KNXBindingConfigItem from) { if (from == null) { return null; } return from.itemName; } }); } } /* * (non-Javadoc) * * @see org.openhab.binding.knx.config.KNXBindingProvider#isCommandGA(tuwien.auto.calimero.GroupAddress) */ @Override public boolean isCommandGA(final GroupAddress groupAddress) { synchronized (bindingConfigs) { for (BindingConfig config : bindingConfigs.values()) { KNXBindingConfig knxConfig = (KNXBindingConfig) config; for (KNXBindingConfigItem configItem : knxConfig) { if (configItem.allDataPoints.contains(groupAddress)) { if (configItem.mainDataPoint instanceof CommandDP) { if (configItem.mainDataPoint.getMainAddress().equals(groupAddress)) { // the first GA in a CommandDP is always a command GA return true; } else { return false; } } else { // it is a StateDP, so the GA cannot be a command GA return false; } } } } } return false; } /* * (non-Javadoc) * * @see org.openhab.binding.knx.config.KNXBindingProvider#getReadableDatapoints() */ @Override @SuppressWarnings("unchecked") public Iterable<Datapoint> getReadableDatapoints() { synchronized (bindingConfigs) { Iterable<KNXBindingConfig> configList = Iterables.filter(Iterables.concat(bindingConfigs.values()), KNXBindingConfig.class); Iterable<KNXBindingConfigItem> configItemList = Iterables.filter(Iterables.concat(configList), KNXBindingConfigItem.class); Iterable<KNXBindingConfigItem> filteredBindingConfigs = Iterables.filter(configItemList, new Predicate<KNXBindingConfigItem>() { @Override public boolean apply(KNXBindingConfigItem input) { if (input == null) { return false; } return input.readableDataPoint != null; } }); return Iterables.transform(filteredBindingConfigs, new Function<KNXBindingConfigItem, Datapoint>() { @Override public Datapoint apply(KNXBindingConfigItem from) { return from.readableDataPoint; } }); } } /* * (non-Javadoc) * * @see * org.openhab.binding.knx.config.KNXBindingProvider#isAutoRefreshEnabled(tuwien.auto.calimero.datapoint.Datapoint) */ @Override public boolean isAutoRefreshEnabled(Datapoint dataPoint) { return (getAutoRefreshTime(dataPoint) != 0); } /* * (non-Javadoc) * * @see * org.openhab.binding.knx.config.KNXBindingProvider#getAutoRefreshTime(tuwien.auto.calimero.datapoint.Datapoint) */ @Override public int getAutoRefreshTime(Datapoint dataPoint) { synchronized (bindingConfigs) { for (BindingConfig config : bindingConfigs.values()) { KNXBindingConfig knxConfig = (KNXBindingConfig) config; for (KNXBindingConfigItem configItem : knxConfig) { if ((configItem.readableDataPoint != null) && (configItem.readableDataPoint.equals(dataPoint))) { return configItem.autoRefreshInSecs; } } } } return 0; } /* * (non-Javadoc) * * @see org.openhab.core.autoupdate.AutoUpdateBindingProvider#autoUpdate(java.lang.String) */ @Override public Boolean autoUpdate(String itemName) { BindingConfig config = bindingConfigs.get(itemName); if (config instanceof KNXBindingConfig) { KNXBindingConfig knxConfig = (KNXBindingConfig) config; Iterator<KNXBindingConfigItem> it = knxConfig.iterator(); while (it.hasNext()) { KNXBindingConfigItem item = it.next(); if (item.allDataPoints.getDatapoints().size() > 1) { // If the datapoint is a CommandDP, the first GA is the command GA, all other are listening GAs. // If the datapoint is a StateDP, all GAs are listening GAs. // If we have a single DPT configured with a command GA and at least one listening GA, // we deactivate the auto-update as we assume that status updates after a command // will come from KNX. return false; } } } return null; } /** * This is the main method that takes care of parsing a binding configuration * string for a given item. It returns a collection of {@link BindingConfig} * instances, which hold all relevant data about the binding to KNX of an item. * * @param item the item for which the binding configuration string is provided * @param bindingConfig a string which holds the binding information * @return a knx binding config, a collection of {@link KNXBindingConfigItem} * instances, which hold all relevant data about the binding * @throws BindingConfigParseException if the configuration string has no valid syntax */ protected KNXBindingConfig parseBindingConfigString(Item item, String bindingConfig) throws BindingConfigParseException { KNXBindingConfig config = new KNXBindingConfig(); String[] datapointConfigs = bindingConfig.trim().split(","); // we can have one datapoint per accepted command type of this item for (int i = 0; i < datapointConfigs.length; i++) { try { String datapointConfig = datapointConfigs[i].trim(); KNXBindingConfigItem configItem = new KNXBindingConfigItem(); configItem.itemName = item.getName(); if (datapointConfig.split("<").length > 2) { throw new BindingConfigParseException("Only one readable GA allowed."); } String[] dataPoints = datapointConfig.split("\\+"); for (int j = 0; j < dataPoints.length; ++j) { String dataPoint = dataPoints[j].trim(); // If dataPoint is empty, we most likely have "pure" listening DP (+x/y/z). // Just skip it, it will be handle in the next iteration. if (dataPoint.isEmpty()) { continue; } boolean isReadable = false; int autoRefreshTimeInSecs = 0; // check for the readable flag if (dataPoint.startsWith("<")) { isReadable = true; dataPoint = dataPoint.substring(1); // check for the auto refresh parameter if (dataPoint.startsWith("(")) { int endIndex = dataPoint.indexOf(")"); if (endIndex > -1) { dataPoint = dataPoint.substring(1); if (endIndex > 1) { try { autoRefreshTimeInSecs = Integer.parseInt(dataPoint.substring(0, endIndex - 1)); dataPoint = dataPoint.substring(endIndex); if (autoRefreshTimeInSecs == 0) { throw new BindingConfigParseException("Autorefresh time cannot be 0."); } } catch (NumberFormatException nfe) { throw new BindingConfigParseException( "Autorefresh time must be a number, but was '" + dataPoint.substring(1, endIndex) + "'."); } } else { throw new BindingConfigParseException( "Autorefresh time parameter: missing time. Empty brackets are not allowed."); } } else { throw new BindingConfigParseException( "Closing ')' missing on autorefresh time parameter."); } } } // find the DPT for this entry String[] segments = dataPoint.split(":"); Class<? extends Type> typeClass = null; String dptID = null; if( segments.length == 1 ) { //DatapointID NOT specified in binding config, so try to guess it typeClass = item.getAcceptedCommandTypes().size() > 0 ? item.getAcceptedCommandTypes().get(i) : item.getAcceptedDataTypes().size() > 1 ? item.getAcceptedDataTypes().get(i) : item.getAcceptedDataTypes().get(0); dptID = getDefaultDPTId(typeClass); } else { //DatapointID specified in binding config, so use it dptID = segments[0]; } if (dptID == null || dptID.trim().isEmpty()) { throw new BindingConfigParseException( "No DPT could be determined for the type '" + typeClass.getSimpleName() + "'."); } // check if this DPT is supported if (KNXCoreTypeMapper.toTypeClass(dptID) == null) { throw new BindingConfigParseException("DPT " + dptID + " is not supported by the KNX binding."); } String ga = (segments.length == 1) ? segments[0].trim() : segments[1].trim(); // determine start/stop behavior Boolean startStopBehavior = Boolean.FALSE; if (ga.endsWith(START_STOP_MARKER_SUFFIX)) { startStopBehavior = Boolean.TRUE; ga = ga.substring(0, ga.length() - START_STOP_MARKER_SUFFIX.length()); } // create group address and datapoint GroupAddress groupAddress = new GroupAddress(ga); configItem.startStopMap.put(groupAddress, startStopBehavior); Datapoint dp; if (j != 0 || item.getAcceptedCommandTypes().size() == 0) { dp = new StateDP(groupAddress, item.getName(), 0, dptID); } else { dp = new CommandDP(groupAddress, item.getName(), 0, dptID); } // assign datapoint to configuration item if (configItem.mainDataPoint == null) { configItem.mainDataPoint = dp; } if (isReadable) { configItem.readableDataPoint = dp; if (autoRefreshTimeInSecs > 0) { configItem.autoRefreshInSecs = autoRefreshTimeInSecs; } } if (!configItem.allDataPoints.contains(dp)) { configItem.allDataPoints.add(dp); } else { throw new BindingConfigParseException( "Datapoint '" + dp.getDPT() + "' already exists for item '" + item.getName() + "'."); } } config.add(configItem); } catch (IndexOutOfBoundsException e) { throw new BindingConfigParseException( "No more than " + i + " datapoint definitions are allowed for this item."); } catch (KNXFormatException e) { throw new BindingConfigParseException(e.getMessage()); } } return config; } /** * Returns a default datapoint type id for a type class. * * @param typeClass the type class * @return the default datapoint type id */ private String getDefaultDPTId(Class<? extends Type> typeClass) { return KNXCoreTypeMapper.toDPTid(typeClass); } /** * This is an internal container to gather all config items for one opeHAB item. * * @author Kai Kreuzer * */ @SuppressWarnings("serial") /* default */ static class KNXBindingConfig extends LinkedList<KNXBindingConfigItem>implements BindingConfig { } /** * This is an internal data structure to store information from the binding config strings and use it to answer the * requests to the KNX binding provider. * * @author Kai Kreuzer * */ /* default */ static class KNXBindingConfigItem { public String itemName; public Datapoint mainDataPoint = null; public Datapoint readableDataPoint = null; public DatapointMap allDataPoints = new DatapointMap(); public int autoRefreshInSecs = 0; public Map<GroupAddress, Boolean> startStopMap = new HashMap<GroupAddress, Boolean>(); } /** * Determines if the given group address is marked for start-stop dimming. * * @param groupAddress the group address to check start-stop dimming for * @returns true, if the given group address is marked for start-stop dimming, false otherwise. */ @Override public boolean isStartStopGA(GroupAddress groupAddress) { synchronized (bindingConfigs) { for (BindingConfig config : bindingConfigs.values()) { KNXBindingConfig knxConfig = (KNXBindingConfig) config; for (KNXBindingConfigItem configItem : knxConfig) { Boolean startStopBehavior = configItem.startStopMap.get(groupAddress); if (startStopBehavior != null) { return startStopBehavior; } } } } return false; } }