/*
* Dog - Network Driver
*
* Copyright (c) 2011-2013 Dario Bonino and Luigi De Russis
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package it.polito.elite.dog.drivers.knx.network;
import it.polito.elite.dog.core.library.util.LogHelper;
import it.polito.elite.dog.drivers.knx.network.info.KnxIPDeviceInfo;
import it.polito.elite.dog.drivers.knx.network.info.KnxIPInfo;
import it.polito.elite.dog.drivers.knx.network.interfaces.KnxIPNetwork;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.log.LogService;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.datapoint.CommandDP;
import tuwien.auto.calimero.datapoint.Datapoint;
import tuwien.auto.calimero.dptxlator.DPT;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteInteger;
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
import tuwien.auto.calimero.dptxlator.TranslatorTypes.MainType;
import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.exception.KNXFormatException;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
/**
* The network driver for KNX networks using the KNXNet/IP protocol
*
* @author <a href="mailto:dario.bonino@polito.it">Dario Bonino</a>
* @see <a href="http://elite.polito.it">http://elite.polito.it</a>
*
*/
public class KnxIpDriverImpl implements KnxIPNetwork, ManagedService, ProcessListener
{
// the number of connection trials
private int nConnectionTrials;
private int trialsDone;
// the time that must occur between two subsequent trials
private int betweenTrialTimeMillis;
// the time for the watchdog to detect disconnections
private int watchdogTime;
// a reference to the watchdog timer
private Timer watchdog;
// a reference to the connection trials timer
private Timer connectionTrialsTimer;
// the dog ip address
private Set<InetSocketAddress> dogIpAddr;
// the netmask for comparing ip addresses
private String dogMask;
// the map groupAddress/DPT
private Map<KnxIPDeviceInfo, DPT> knxDeviceInfo2DPT;
// the map groupAddres/device-specific driver
private Map<KnxIPDeviceInfo, KnxIPDriverInstance> knxDeviceInfo2Driver;
// the KnxIp gateway to device map
private Map<InetSocketAddress, Set<KnxIPDeviceInfo>> gateway2Device;
// the KnxIp gateway to process communicator map
private Map<InetSocketAddress, ProcessCommunicator> gateway2ProcessCommunicator;
// the KnxIp gateway to process communicator map
private Map<InetSocketAddress, KNXNetworkLinkIP> gateway2IPLink;
// the Ipaddress to IP socket map
private Map<InetAddress, InetSocketAddress> ip2sock;
// a reference to the bundle context
private BundleContext bundleContext;
// the service registration handle
private ServiceRegistration<?> regServiceKnxIp;
// the driver logger
private LogHelper logger;
// the log identifier, unique for the class
public static String logId = "[KnxIpDriverImpl]: ";
public KnxIpDriverImpl()
{
// intentionally left empty
}
public void activate(BundleContext bundleContext)
{
// create a logger
this.logger = new LogHelper(bundleContext);
// set the number of done trials to 0
this.trialsDone = 0;
// store the reference to the bundle context
this.bundleContext = bundleContext;
// create the groupAddress to DPT map
this.knxDeviceInfo2DPT = new HashMap<KnxIPDeviceInfo, DPT>();
// create the groupAddress to KnxIPDriver map
this.knxDeviceInfo2Driver = new HashMap<KnxIPDeviceInfo, KnxIPDriverInstance>();
// initialize the gateway to device map
this.gateway2Device = new HashMap<InetSocketAddress, Set<KnxIPDeviceInfo>>();
// initialize the gateway to process communicator map
this.gateway2ProcessCommunicator = new HashMap<InetSocketAddress, ProcessCommunicator>();
// initialize the gateway to iplink map
this.gateway2IPLink = new HashMap<InetSocketAddress, KNXNetworkLinkIP>();
// initialize the ipaddress - ipsocket map
this.ip2sock = new HashMap<InetAddress, InetSocketAddress>();
// initialize the set of dog ip addresses
this.dogIpAddr = new HashSet<InetSocketAddress>();
// add the missing dptxlator
this.addAdditionalDPTXlators();
}
public void deactivate()
{
// unregister the service and closes connections
this.unRegister();
// empty the inner data structures
this.dogIpAddr.clear();
this.gateway2Device.clear();
this.gateway2IPLink.clear();
this.gateway2ProcessCommunicator.clear();
this.ip2sock.clear();
this.knxDeviceInfo2DPT.clear();
this.knxDeviceInfo2Driver.clear();
// stop the network watchdog
if (this.watchdog != null)
this.watchdog.cancel();
// stop the connection trial timer
if (this.connectionTrialsTimer != null)
this.connectionTrialsTimer.cancel();
// set everything at null?
this.dogIpAddr = null;
this.betweenTrialTimeMillis = 0;
this.bundleContext = null;
this.connectionTrialsTimer = null;
this.dogIpAddr = null;
this.dogMask = null;
this.gateway2Device = null;
this.gateway2IPLink = null;
this.gateway2ProcessCommunicator = null;
this.ip2sock = null;
this.knxDeviceInfo2DPT = null;
this.knxDeviceInfo2Driver = null;
this.logger = null;
this.nConnectionTrials = 0;
this.regServiceKnxIp = null;
this.trialsDone = 0;
this.watchdog = null;
this.watchdogTime = 0;
}
/**
* Unregisters this driver from the OSGi framework and stops the active
* threads (Timers, in this case)
*/
public void unRegister()
{
if (this.regServiceKnxIp != null)
{
this.regServiceKnxIp.unregister();
}
if (this.connectionTrialsTimer != null)
{
this.connectionTrialsTimer.cancel();
}
if (this.watchdog != null)
{
this.watchdog.cancel();
}
// check if any process communicators are open... and close them
for (ProcessCommunicator pc : this.gateway2ProcessCommunicator.values())
{
if (pc != null)
{
pc.detach();
}
}
// check if any ip link is open... and close them
for (KNXNetworkLinkIP ipLink : this.gateway2IPLink.values())
{
if (ipLink != null && ipLink.isOpen())
{
ipLink.close();
}
}
}
/****************** Managed Service ****************/
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary properties) throws ConfigurationException
{
if (properties != null)
{
try
{
// get the dog ip
String dogIp = (String) properties.get("dogIp");
// split over the commas
String ipAddresses[] = dogIp.split(",");
int dogPort = Integer.parseInt((String) properties.get("dogPort"));
// store all the dog IP addresses
for (int i = 0; i < ipAddresses.length; i++)
{
// check not null
if ((dogIp != null) && (!dogIp.isEmpty()) && (dogPort > 0) && (dogPort < 65536))
{
this.dogIpAddr.add(new InetSocketAddress(ipAddresses[i].trim(), dogPort++));
}
}
// get the dog netmask
this.dogMask = (String) properties.get("dogMask");
// store the time between subsequent connection trials
this.betweenTrialTimeMillis = Integer.parseInt((String) properties.get("betweenTrialTimeMillis"));
// store the maximum number of connection trials
this.nConnectionTrials = Integer.parseInt((String) properties.get("numTry"));
// store the watchdog time
this.watchdogTime = Integer.parseInt((String) properties.get("watchdogTimeSeconds")) * 1000;
// register the driver service if not already registered
if (this.regServiceKnxIp == null)
this.regServiceKnxIp = this.bundleContext.registerService(KnxIPNetwork.class.getName(), this, null);
}
catch (NumberFormatException nfe)
{
this.logger.log(LogService.LOG_ERROR, nfe.getMessage());
}
catch (Exception e)
{
this.logger.log(LogService.LOG_ERROR, e.getMessage());
}
}
}
/****************** KnxIpNetwork *******************/
@Override
public void read(KnxIPDeviceInfo deviceInfo)
{
// get the device datapoint, if available
DPT deviceDPT = this.knxDeviceInfo2DPT.get(deviceInfo);
// gete the device driver if available
KnxIPDriverInstance deviceDriver = this.knxDeviceInfo2Driver.get(deviceInfo);
// if both the device DPT and Driver are available, send the read
// command
if ((deviceDPT != null) && (deviceDriver != null))
{
// read a value from the given device address
Datapoint dpt;
try
{
// create the command datapoitn needed to send the read command
dpt = new CommandDP(new GroupAddress(deviceInfo.getGroupAddress()), "");
// set the datapoint translator
dpt.setDPT(0, deviceDPT.getID());
// read and send back the read result by calling the new message
// from house command
this.read(dpt, deviceDriver, this.ip2sock.get(deviceInfo.getGatewayIPAddress()));
}
catch (KNXFormatException e)
{
this.logger
.log(LogService.LOG_WARNING,
KnxIpDriverImpl.logId
+ "Unable to correctly interpret the given device address, please check if the given group address ["
+ deviceInfo + "] has the right group address form\n" + "Nested exception:" + e);
}
}
}
@Override
public void write(KnxIPDeviceInfo deviceInfo, String commandValue)
{
// get the device datapoint, if available
DPT deviceDPT = this.knxDeviceInfo2DPT.get(deviceInfo);
// gete the device driver if available
KnxIPDriverInstance deviceDriver = this.knxDeviceInfo2Driver.get(deviceInfo);
// if both the device DPT and Driver are available, send the read
// command
if ((deviceDPT != null) && (deviceDriver != null))
{
// read a value from the given device address
Datapoint dpt;
try
{
// create the command datapoint needed to send the read command
dpt = new CommandDP(new GroupAddress(deviceInfo.getGroupAddress()), "");
// set the datapoint translator
dpt.setDPT(0, deviceDPT.getID());
// send the command to the given group address (and do not wait
// for response...)
this.write(dpt, commandValue, this.ip2sock.get(deviceInfo.getGatewayIPAddress()));
}
catch (KNXFormatException e)
{
this.logger
.log(LogService.LOG_WARNING,
KnxIpDriverImpl.logId
+ "Unable to correctly interpret the given device address, please check if the given group address ["
+ deviceInfo + "] has the right group address form\n" + "Nested exception:" + e);
}
}
}
@Override
public void addDriver(KnxIPDeviceInfo device, int mainNumber, DPT deviceDPT, KnxIPDriverInstance driver)
{
// store the associations group address / DPT
this.knxDeviceInfo2DPT.put(device, deviceDPT);
// store the associations group address / driver
this.knxDeviceInfo2Driver.put(device, driver);
// get the gateway address
InetSocketAddress gwAddress = new InetSocketAddress(device.getGatewayIPAddress(), KnxIPInfo.DEFAULT_PORT);
// if the gateway is not available, open a connection to it
if (!this.gateway2Device.containsKey(gwAddress))
{
this.openKnxIPTunnel(gwAddress);
this.gateway2Device.put(gwAddress, new HashSet<KnxIPDeviceInfo>());
this.ip2sock.put(gwAddress.getAddress(), gwAddress);
}
this.gateway2Device.get(gwAddress).add(device);
}
@Override
public void removeDriver(KnxIPDeviceInfo device)
{
// removes the given device address from the entries of the groupAddress
// to DPT and to Driver tables
this.knxDeviceInfo2DPT.remove(device);
this.knxDeviceInfo2Driver.remove(device);
}
public void removeDriver(KnxIPDriverInstance driver)
{
//create the set of device info to remove as consequence of driver removal
Set<KnxIPDeviceInfo> toRemove = new HashSet<KnxIPDeviceInfo>();
//fill the set of device info to remove
for(KnxIPDeviceInfo devInfo : this.knxDeviceInfo2Driver.keySet())
{
if(this.knxDeviceInfo2Driver.get(devInfo).equals(driver))
toRemove.add(devInfo);
}
//remove all
for(KnxIPDeviceInfo devInfo : toRemove)
{
this.knxDeviceInfo2Driver.remove(devInfo);
this.knxDeviceInfo2DPT.remove(devInfo);
//search for entries in the gateway maps
InetAddress gwAddress = devInfo.getGatewayIPAddress();
//get the corresponding socket address
InetSocketAddress sockGwAddress = this.ip2sock.get(gwAddress);
//remove the entries
Set<KnxIPDeviceInfo> gwDevices = this.gateway2Device.get(sockGwAddress);
gwDevices.remove(devInfo);
//check if empty
if(gwDevices.isEmpty())
{
//remove the gateway entry
this.gateway2Device.remove(sockGwAddress);
this.gateway2ProcessCommunicator.get(sockGwAddress);
this.gateway2ProcessCommunicator.remove(sockGwAddress);
this.gateway2IPLink.get(sockGwAddress).close();
this.gateway2IPLink.remove(sockGwAddress);
}
}
}
/************************* KnxIp Communication Stuff ***********************/
private void openKnxIPTunnel(final InetSocketAddress gwIpAddress)
{
// create a tp settings object for setting up the TP throughput
TPSettings settings = new TPSettings(true);
// open an ipLink towards the gateway
try
{
// find the right ip, if any available
for (InetSocketAddress currentDogIP : this.dogIpAddr)
{
if (this.isSameNetwork(currentDogIP.getAddress(), gwIpAddress.getAddress(), this.dogMask))
{
KNXNetworkLinkIP ipLink = new KNXNetworkLinkIP(KNXNetworkLinkIP.TUNNEL, currentDogIP, gwIpAddress,
false, settings);
// create an ip link towards the given gateway
this.gateway2IPLink.put(gwIpAddress, ipLink);
// open the process communicator
ProcessCommunicator pc = new ProcessCommunicatorImpl(ipLink);
// register this instance as listener for network
// notifications
pc.addProcessListener(this);
// store the pointer to the process communicator
this.gateway2ProcessCommunicator.put(gwIpAddress, pc);
// log the successful connection
this.logger.log(LogService.LOG_INFO, KnxIpDriverImpl.logId
+ "Successfully connected to the KNX gateway...");
this.startWatchdog(gwIpAddress);
break;
}
}
}
catch (KNXException e)
{
// TODO: remove the fixed number of trial definition in the .config
// file and delegate management of re-connection policies to the Dog
// error management bundle
if ((this.trialsDone < this.nConnectionTrials) || (this.nConnectionTrials == 0))
{
// log a warning
this.logger.log(LogService.LOG_WARNING, KnxIpDriverImpl.logId
+ "Unable to connect to the given KNX gateway, retrying in " + this.betweenTrialTimeMillis
+ " ms");
// schedule a new timer to re-call the open function after the
// given trial timeout...
connectionTrialsTimer = new Timer();
connectionTrialsTimer.schedule(new TimerTask() {
@Override
public void run()
{
openKnxIPTunnel(gwIpAddress);
}
}, this.betweenTrialTimeMillis);
// avoid incrementing the number of trials if the
// nConnectionTrials is equal to 0 (i.e. infinite re-trial)
if (this.nConnectionTrials != 0)
this.trialsDone++;
}
else
{
// log a fatal error
this.logger.log(LogService.LOG_ERROR, KnxIpDriverImpl.logId
+ "Unable to connect to the given KNX gateway");
}
}
}
private void startWatchdog(final InetSocketAddress gwIpAddress)
{
if (this.watchdogTime > 0)
{
// start the watchdog timer
this.watchdog = new Timer();
this.watchdog.schedule(new TimerTask() {
@Override
public void run()
{
// log the connection end
logger.log(LogService.LOG_WARNING, KnxIpDriverImpl.logId
+ "Detected network loss, trying to reconnect...");
// close the open connections
gateway2IPLink.get(gwIpAddress).close();
// this detach operation leads to a new connection
// opening...
gateway2ProcessCommunicator.get(gwIpAddress).detach();
}
}, this.watchdogTime);
}
}
/**
* Adds the additional {@link DPTXlator}s needed for the driver to work
*/
private void addAdditionalDPTXlators()
{
// get the initial state of the device
Runnable worker = new Runnable() {
public void run()
{
@SuppressWarnings("unchecked")
Map<Integer, MainType> allMainTypes = TranslatorTypes.getAllMainTypes();
allMainTypes.put(new Integer(14), new TranslatorTypes.MainType(14, DPTXlator4ByteFloat.class,
"4-byte float DPTXlator"));
allMainTypes.put(new Integer(13), new TranslatorTypes.MainType(13, DPTXlator4ByteInteger.class,
"4-byte int DPTXlator"));
logger.log(LogService.LOG_DEBUG, KnxIpDriverImpl.logId + "Loaded additional DPTXlators...");
}
};
Thread workerThread = new Thread(worker);
workerThread.start();
}
/**
* Reads a value from a given datapoint and dispatches back the value to the
* given driver.
*
* @param dpt
* the datapoint to read.
* @param deviceDriver
* the driver to which the read value shall be sent.
*/
private void read(Datapoint dpt, KnxIPDriverInstance deviceDriver, InetSocketAddress gwAddress)
{
try
{
ProcessCommunicator pc = this.gateway2ProcessCommunicator.get(gwAddress);
if (pc != null)
{
// read the value from the network connection
String readValue = pc.read(dpt);
// dispatch the value through the asynchronous message
// dispatching mechanism
deviceDriver.newMessageFromHouse("", dpt.getMainAddress().toString(), readValue);
}
}
catch (KNXException e)
{
this.logger.log(LogService.LOG_WARNING,
KnxIpDriverImpl.logId + "Unable to read value from " + dpt.getMainAddress() + " exception: " + e);
}
}
/**
* writes a value on the given device, represented by a command dpt
*
* @param dpt
* the command dpt representing the device.
* @param commandValue
* the value of the command to send.
*/
private void write(Datapoint dpt, String commandValue, InetSocketAddress gwAddress)
{
try
{
ProcessCommunicator pc = this.gateway2ProcessCommunicator.get(gwAddress);
if (pc != null)
{
pc.write(dpt, commandValue);
}
}
catch (KNXException e)
{
this.logger.log(LogService.LOG_WARNING,
KnxIpDriverImpl.logId + "Unable to write value to " + dpt.getMainAddress() + " exception: " + e);
}
}
/*********************************** Process Listener Events ************************/
@Override
public void detached(DetachEvent arg0)
{
// TODO: check when this event is sent and why...
this.logger.log(LogService.LOG_WARNING, KnxIpDriverImpl.logId
+ "IP connection was lost.... retrying to connect in " + this.betweenTrialTimeMillis + "ms");
// look for detached process communicators and try to re-attach...
for (Entry<InetSocketAddress, KNXNetworkLinkIP> entry : this.gateway2IPLink.entrySet())
{
if (!entry.getValue().isOpen())
{
this.openKnxIPTunnel(entry.getKey());
}
}
}
@Override
public void groupWrite(final ProcessEvent arg0)
{
// search the corresponding gateway
InetSocketAddress gwAddress = null;
for (Entry<InetSocketAddress, ProcessCommunicator> entry : this.gateway2ProcessCommunicator.entrySet())
{
if (entry.getValue().equals((ProcessCommunicator) arg0.getSource()))
{
gwAddress = entry.getKey();
}
}
if (gwAddress != null)
{
// reset the watchdog
if (this.watchdog != null)
{
this.watchdog.cancel();
this.startWatchdog(gwAddress);
}
// get the corresponding DPT if available
Set<KnxIPDeviceInfo> possibleDevices = this.gateway2Device.get(gwAddress);
if (!possibleDevices.isEmpty())
{
// search the right device
for (final KnxIPDeviceInfo deviceInfo : possibleDevices)
{
if (deviceInfo.getGroupAddress().equals(arg0.getDestination().toString()))
{
final DPT currentDPT = this.knxDeviceInfo2DPT.get(deviceInfo);
Runnable workerRunnable = new Runnable() {
@Override
public void run()
{
// if a DPT has been found, decode the message
// and pass-it back to the driver
if (currentDPT != null)
{
try
{
// get the right DPTXlator
DPTXlator translator = TranslatorTypes.createTranslator(currentDPT);
// pass the raw data to the DPXlator
translator.setData(arg0.getASDU());
// get the value back
String value = translator.getValue();
// try to find a driver to dispatch to
KnxIPDriverInstance driver = knxDeviceInfo2Driver.get(deviceInfo);
// if not null dispatch the message,
// otherwise log the
// missing driver
if (driver != null)
{
// dispatch the decoded message...
driver.newMessageFromHouse(arg0.getSourceAddr().toString(), arg0
.getDestination().toString(), value);
// log the value receipt
logger.log(LogService.LOG_DEBUG,
KnxIpDriverImpl.logId
+ "Received low level KNX notification with destination "
+ arg0.getDestination() + " and Value: " + value
+ "\n Dispatched to the registered driver...");
}
else
{
logger.log(LogService.LOG_DEBUG, KnxIpDriverImpl.logId
+ "Received low level KNX notification with destination "
+ arg0.getDestination().toString() + " and value " + value
+ " for which no driver has registered");
}
}
catch (KNXFormatException e1)
{
e1.printStackTrace();
}
catch (KNXException e)
{
e.printStackTrace();
}
}
else
{
// log the unknown message arrival
logger.log(LogService.LOG_DEBUG, KnxIpDriverImpl.logId
+ "Received low level KNX notification with unknown destination "
+ arg0.getDestination().toString());
}
}
};
Thread workerThread = new Thread(workerRunnable);
workerThread.start();
// we assume only one DPT per group address
break;
}
}
}
}
}
/**
* Checks if the given two addresses are the same according to the given
* netmask
*
* @param ip1
* @param ip2
* @param mask
* @return
* @throws Exception
*/
private boolean isSameNetwork(InetAddress ip1, InetAddress ip2, String mask)
{
// get the byte representation of the inet address 1
byte[] a1 = ip1.getAddress();
// get the byte representation of the inet address 2
byte[] a2 = ip2.getAddress();
byte[] m = null;
try
{
// get the byte of the mask inet address
m = InetAddress.getByName(mask).getAddress();
}
catch (Exception e)
{
// handle the exception
return false;
}
// if all the representations are available, check the addresses against
// the netmask
for (int i = 0; i < a1.length; i++)
if ((a1[i] & m[i]) != (a2[i] & m[i]))
return false;
return true;
}
}