/** * 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.bus; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.IllegalClassException; import org.openhab.binding.knx.config.KNXBindingProvider; import org.openhab.binding.knx.config.KNXTypeMapper; import org.openhab.binding.knx.internal.connection.KNXConnection; import org.openhab.binding.knx.internal.connection.KNXConnectionListener; import org.openhab.core.binding.AbstractBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.Type; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tuwien.auto.calimero.DetachEvent; import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.Settings; import tuwien.auto.calimero.datapoint.Datapoint; import tuwien.auto.calimero.exception.KNXException; import tuwien.auto.calimero.process.ProcessCommunicator; import tuwien.auto.calimero.process.ProcessEvent; import tuwien.auto.calimero.process.ProcessListener; /** * This is the central class that takes care of the event exchange between openHAB and KNX. * It is fully connected (read and write) to the openHAB event bus and also has write access * to KNX while as well listening for incoming KNX messages. * * The received messages are converted into the right format for the other bus and published * to it. * * @author Kai Kreuzer * @since 0.3.0 * */ public class KNXBinding extends AbstractBinding<KNXBindingProvider> implements ProcessListener, KNXConnectionListener { private static final Logger logger = LoggerFactory.getLogger(KNXBinding.class); /** to keep track of all KNX type mappers */ protected Collection<KNXTypeMapper> typeMappers = new HashSet<KNXTypeMapper>(); /** * used to store events that we have sent ourselves; we need to remember them for not reacting to them */ private List<String> ignoreEventList = Collections.synchronizedList(new ArrayList<String>()); private KNXBusReaderScheduler mKNXBusReaderScheduler = new KNXBusReaderScheduler(); private boolean mKNXConnectionEstablished; private Map<String, DimmerThread> itemDimmerThreads = Collections .synchronizedMap(new HashMap<String, DimmerThread>()); public void activate(ComponentContext componentContext) { logger.debug("Calimero library version {}", Settings.getLibraryVersion()); logger.trace("KNXBinding: activating"); KNXConnection.addConnectionListener(this); mKNXBusReaderScheduler.start(); } public void deactivate(ComponentContext componentContext) { logger.trace("KNXBinding: deactivating"); KNXConnection.removeConnectionListener(this); for (KNXBindingProvider provider : providers) { provider.removeBindingChangeListener(this); } providers.clear(); mKNXBusReaderScheduler.stop(); KNXConnection.disconnect(); } public void addKNXTypeMapper(KNXTypeMapper typeMapper) { this.typeMappers.add(typeMapper); } public void removeKNXTypeMapper(KNXTypeMapper typeMapper) { this.typeMappers.remove(typeMapper); } /* * (non-Javadoc) * * @see org.openhab.core.binding.AbstractBinding#internalReceiveCommand(java.lang.String, * org.openhab.core.types.Command) */ @Override protected void internalReceiveCommand(String itemName, Command command) { logger.trace("Received command (item='{}', command='{}')", itemName, command.toString()); if (!isEcho(itemName, command)) { writeToKNX(itemName, command); } } /* * (non-Javadoc) * * @see org.openhab.core.binding.AbstractBinding#internalReceiveUpdate(java.lang.String, * org.openhab.core.types.State) */ @Override protected void internalReceiveUpdate(String itemName, State newState) { logger.trace("Received update (item='{}', state='{}')", itemName, newState.toString()); if (!isEcho(itemName, newState)) { writeToKNX(itemName, newState); } } private boolean isEcho(String itemName, Type type) { String ignoreEventListKey = itemName + type.toString(); if (ignoreEventList.remove(ignoreEventListKey)) { logger.trace( "We received this event (item='{}', state='{}') from KNX, so we don't send it back again -> ignore!", itemName, type.toString()); return true; } else { return false; } } private void writeToKNX(String itemName, Type value) { Iterable<Datapoint> datapoints = getDatapoints(itemName, value.getClass()); if (datapoints != null) { ProcessCommunicator pc = KNXConnection.getCommunicator(); if (pc != null) { for (Datapoint datapoint : datapoints) { try { pc.write(datapoint, toDPTValue(value, datapoint.getDPT())); logger.debug("Wrote value '{}' to datapoint '{}'", value, datapoint); } catch (KNXException e) { logger.warn( "Value '{}' could not be sent to the KNX bus using datapoint '{}' - retrying one time: {}", new Object[] { value, datapoint, e.getMessage() }); try { // do a second try, maybe the reconnection was successful pc = KNXConnection.getCommunicator(); pc.write(datapoint, toDPTValue(value, datapoint.getDPT())); logger.debug("Wrote value '{}' to datapoint '{}' on second try", value, datapoint); } catch (KNXException e1) { logger.error( "Value '{}' could not be sent to the KNX bus using datapoint '{}' - giving up after second try: {}", new Object[] { value, datapoint, e1.getMessage() }); } } } } } } /* * (non-Javadoc) * * @see tuwien.auto.calimero.process.ProcessListener#groupWrite(tuwien.auto.calimero.process.ProcessEvent) */ /** * If <code>knx:ignorelocalevents=true</code> is set in configuration, it prevents internal events * coming from 'openHAB event bus' a second time to be sent back to the 'openHAB event bus'. * * @param e the {@link ProcessEvent} to handle. */ @Override public void groupWrite(ProcessEvent e) { logger.debug("Received groupWrite Event."); if (!(KNXConnection.getIgnoreLocalSourceEvents() && e.getSourceAddr().toString().equalsIgnoreCase(KNXConnection.getLocalSourceAddr()))) { readFromKNX(e); } else { logger.debug("Ignoring local Event, received from my local Source address {} for Group address {}.", e.getSourceAddr().toString(), e.getDestination().toString()); } } /* * (non-Javadoc) * * @see tuwien.auto.calimero.process.ProcessListener#detached(tuwien.auto.calimero.DetachEvent) */ @Override public void detached(DetachEvent e) { logger.error("Received detach Event."); } /** * Handles the given {@link ProcessEvent}. After finding the corresponding * Item (by iterating through all known group addresses) this Item is updated. * Each item is added to a special list to identify and avoid echo's in * the <code>receiveUpdate</code> and <code>receiveCommand</code> methods. * * @param e the {@link ProcessEvent} to handle. */ private void readFromKNX(ProcessEvent e) { try { GroupAddress destination = e.getDestination(); byte[] asdu = e.getASDU(); if (asdu.length == 0) { return; } String[] itemList = getItemNames(destination); if (itemList.length == 0) { logger.debug("Received telegram for unknown group address {}", destination.toString()); } for (String itemName : itemList) { Iterable<Datapoint> datapoints = getDatapoints(itemName, destination); if (datapoints != null) { for (Datapoint datapoint : datapoints) { Type type = getType(datapoint, asdu); if (type != null) { if (type instanceof Command && isStartStopEnabled(itemName, destination, datapoint)) { if (isDimmerThreadRunning(itemName) && type == IncreaseDecreaseType.INCREASE) { stopDimmerThread(itemName); } else { startDimmerThread(destination, itemName, (Command) type); } } else { sendTypeToItemButNotToKnx(destination, itemName, type); } } else { final char[] hexCode = "0123456789ABCDEF".toCharArray(); StringBuilder sb = new StringBuilder(2 + asdu.length * 2); sb.append("0x"); for (byte b : asdu) { sb.append(hexCode[(b >> 4) & 0xF]); sb.append(hexCode[(b & 0xF)]); } logger.debug( "Ignoring KNX bus data: couldn't transform to an openHAB type (not supported). Destination='{}', datapoint='{}', data='{}'", new Object[] { destination.toString(), datapoint.toString(), sb.toString() }); } } } } } catch (RuntimeException re) { logger.error("Error while receiving event from KNX bus: " + re.toString()); } } private boolean isDimmerThreadRunning(String itemName) { DimmerThread dimmerThread = itemDimmerThreads.get(itemName); return dimmerThread != null && dimmerThread.isRunning(); } private void sendTypeToItemButNotToKnx(GroupAddress destination, String itemName, Type type) { // we need to make sure that we won't send out this event to // the knx bus again, when receiving it on the openHAB bus ignoreEventList.add(itemName + type.toString()); logger.trace("Added event (item='{}', type='{}') to the ignore event list", itemName, type.toString()); if (type instanceof Command && isCommandGA(destination)) { eventPublisher.postCommand(itemName, (Command) type); } else if (type instanceof State) { eventPublisher.postUpdate(itemName, (State) type); } else { throw new IllegalClassException("Cannot process datapoint of type " + type.toString()); } logger.trace("Processed event (item='{}', type='{}', destination='{}')", itemName, type.toString(), destination.toString()); } private boolean isStopCommand(byte[] asdu) { return asdu.length > 0 && (asdu[0] == 0x00 || asdu[0] == 0x08); } private boolean isStartStopEnabled(String itemName, GroupAddress destination, Datapoint datapoint) { for (KNXBindingProvider provider : providers) { Iterable<Datapoint> datapoints = provider.getDatapoints(itemName, destination); if (datapoints != null) { for (Datapoint dp : datapoints) { if (dp.equals(datapoint)) { return provider.isStartStopGA(destination); } } } } return false; } /* * (non-Javadoc) * * @see org.openhab.core.binding.AbstractBinding#bindingChanged(org.openhab.core.binding.BindingProvider, * java.lang.String) */ @Override public void bindingChanged(BindingProvider provider, String itemName) { logger.trace("bindingChanged() for item {} msg received.", itemName); if (mKNXConnectionEstablished) { if (provider instanceof KNXBindingProvider) { KNXBindingProvider knxProvider = (KNXBindingProvider) provider; for (Datapoint datapoint : knxProvider.getReadableDatapoints()) { if (datapoint.getName().equals(itemName)) { logger.debug("Initializing read of item {}.", itemName); if (!mKNXBusReaderScheduler.scheduleRead(datapoint, knxProvider.getAutoRefreshTime(datapoint))) { logger.warn("Couldn't add to KNX bus reader scheduler (bindingChanged, datapoint='{}')", datapoint); } break; } } } } } /* * (non-Javadoc) * * @see org.openhab.core.binding.AbstractBinding#allBindingsChanged(org.openhab.core.binding.BindingProvider) */ @Override public void allBindingsChanged(BindingProvider provider) { logger.trace("allBindingsChanged() msg received."); if (mKNXConnectionEstablished) { logger.debug("Initializing readable DPs."); if (provider instanceof KNXBindingProvider) { KNXBindingProvider knxProvider = (KNXBindingProvider) provider; mKNXBusReaderScheduler.clear(); for (Datapoint datapoint : knxProvider.getReadableDatapoints()) { mKNXBusReaderScheduler.readOnce(datapoint); int autoRefreshTimeInSecs = knxProvider.getAutoRefreshTime(datapoint); if (autoRefreshTimeInSecs > 0) { if (!mKNXBusReaderScheduler.scheduleRead(datapoint, knxProvider.getAutoRefreshTime(datapoint))) { logger.warn("Couldn't add to KNX bus reader scheduler (allBindingsChanged, datapoint='{}')", datapoint); } } } } } } /* * (non-Javadoc) * * @see org.openhab.binding.knx.internal.connection.KNXConnectionListener#connectionEstablished() */ @Override public void connectionEstablished() { logger.trace("connectionEstablished() msg received. Initializing readable DPs."); mKNXConnectionEstablished = true; for (KNXBindingProvider knxProvider : providers) { for (Datapoint datapoint : knxProvider.getReadableDatapoints()) { mKNXBusReaderScheduler.readOnce(datapoint); int autoRefreshTimeInSecs = knxProvider.getAutoRefreshTime(datapoint); if (autoRefreshTimeInSecs > 0) { if (!mKNXBusReaderScheduler.scheduleRead(datapoint, autoRefreshTimeInSecs)) { logger.warn("Couldn't add to KNX bus reader scheduler (connectionEstablished, datapoint='{}')", datapoint); } } } } } /* * (non-Javadoc) * * @see org.openhab.binding.knx.internal.connection.KNXConnectionListener#connectionLost() */ @Override public void connectionLost() { logger.trace("connectionLost() msg received."); mKNXConnectionEstablished = false; mKNXBusReaderScheduler.clear(); } /** * Determines whether the given <code>groupAddress</code> is the address which * will be interpreted as the command type. This method iterates over all * registered KNX binding providers to find the result. * * @param groupAddress the group address to check * @return true, if it is a command GA */ private boolean isCommandGA(GroupAddress groupAddress) { for (KNXBindingProvider provider : providers) { if (!provider.isCommandGA(groupAddress)) { return false; } } return true; } /** * Returns all listening item names. This method iterates over all registered KNX binding providers and aggregates * the result. * * @param groupAddress * the group address that the items are listening to * @return an array of all listening items */ private String[] getItemNames(GroupAddress groupAddress) { List<String> itemNames = new ArrayList<String>(); for (KNXBindingProvider provider : providers) { for (String itemName : provider.getListeningItemNames(groupAddress)) { itemNames.add(itemName); } } return itemNames.toArray(new String[itemNames.size()]); } /** * Returns the datapoints for a given item and group address. This method iterates over all registered KNX binding * providers to find the result. * * @param itemName * the item name for the datapoint * @param groupAddress * the group address associated to the datapoint * @return the datapoints which corresponds to the given item and group address */ private Iterable<Datapoint> getDatapoints(String itemName, GroupAddress groupAddress) { for (KNXBindingProvider provider : providers) { Iterable<Datapoint> datapoints = provider.getDatapoints(itemName, groupAddress); if (datapoints != null) { return datapoints; } } return null; } /** * Transforms the raw KNX bus data of a given datapoint into an openHAB type (command or state) * * @param datapoint * the datapoint to which the data belongs * @param asdu * the byte array of the raw data from the KNX bus * @return the openHAB command or state that corresponds to the data */ private Type getType(Datapoint datapoint, byte[] asdu) { for (KNXTypeMapper typeMapper : typeMappers) { Type type = typeMapper.toType(datapoint, asdu); if (type != null) { return type; } } return null; } /** * Returns the datapoints for a given item and type class. This method iterates over all registered KNX binding * providers to find the result. * * @param itemName * the item name for the datapoints * @param typeClass * the type class associated to the datapoints * @return the datapoints which corresponds to the given item and type class */ private Iterable<Datapoint> getDatapoints(final String itemName, final Class<? extends Type> typeClass) { Set<Datapoint> datapoints = new HashSet<Datapoint>(); for (KNXBindingProvider provider : providers) { for (Datapoint datapoint : provider.getDatapoints(itemName, typeClass)) { datapoints.add(datapoint); } } if (datapoints.isEmpty()) { logger.warn("no compatible datapoint found for item {} ({}), check item configuration", itemName, typeClass.getName()); } else { logger.trace("found {} compatible datapoints for item {} ({})", datapoints.size(), itemName, typeClass.getName()); } return datapoints; } /** * Transforms an openHAB type (command or state) into a datapoint type value for the KNX bus. * * @param type * the openHAB command or state to transform * @param dpt * the datapoint type to which should be converted * * @return the corresponding KNX datapoint type value as a string */ private String toDPTValue(Type type, String dpt) { for (KNXTypeMapper typeMapper : typeMappers) { String value = typeMapper.toDPTValue(type, dpt); if (value != null) { return value; } } return null; } private void stopDimmerThread(String item) { DimmerThread dimmerThread = itemDimmerThreads.remove(item); if (dimmerThread != null) { dimmerThread.stopRunning(); } } private void startDimmerThread(GroupAddress destination, String item, Command type) { logger.trace("Starting new dimmer thread for item {}.", item); DimmerThread dimmerThread = new DimmerThread(destination, item, type); itemDimmerThreads.put(item, dimmerThread); dimmerThread.start(); } private class DimmerThread extends Thread { private static final int MAX_LOOPS = 100; private static final long SLEEP_PERIOD_MS = 500; private GroupAddress destination; private String item; private Command command; private boolean running = true; private int currentLoop = 0; public DimmerThread(GroupAddress destination, String item, Command type) { this.destination = destination; this.item = item; this.command = type; setDaemon(true); setName("DimmerThread"); } public void stopRunning() { running = false; } @Override public void run() { while (mayRun()) { logger.debug("Post new value {} for items {}", command, item); sendTypeToItemButNotToKnx(destination, item, command); eventPublisher.postCommand(item, command); try { Thread.sleep(SLEEP_PERIOD_MS); } catch (InterruptedException e) { logger.warn("DimmerThread got interrupted. This should not happen.", e); } } this.running = false; logger.trace("Dimmer thread finished."); } public boolean isRunning() { return running; } private boolean mayRun() { return running && (currentLoop <= MAX_LOOPS); } } }