/*
* Copyright 2011-16 Fraunhofer ISE
*
* This file is part of OpenMUC.
* For more information visit http://www.openmuc.org
*
* OpenMUC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenMUC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenMUC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openmuc.framework.driver.knx;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openmuc.framework.config.ArgumentSyntaxException;
import org.openmuc.framework.config.ChannelScanInfo;
import org.openmuc.framework.data.Flag;
import org.openmuc.framework.data.Record;
import org.openmuc.framework.driver.knx.value.KnxValue;
import org.openmuc.framework.driver.spi.ChannelRecordContainer;
import org.openmuc.framework.driver.spi.ChannelValueContainer;
import org.openmuc.framework.driver.spi.Connection;
import org.openmuc.framework.driver.spi.ConnectionException;
import org.openmuc.framework.driver.spi.RecordsReceivedListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.exception.KNXFormatException;
import tuwien.auto.calimero.exception.KNXTimeoutException;
import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
public class KnxConnection implements Connection {
private static Logger logger = LoggerFactory.getLogger(KnxConnection.class);
private static final int DEFAULT_PORT = 3671;
private static final int DEFAULT_TIMEOUT = 2;
private KNXNetworkLink knxNetworkLink;
private ProcessCommunicator processCommunicator;
private KnxProcessListener processListener;
private int responseTimeout;
private String name;
KnxConnection(String deviceAddress, String settings, int timeout)
throws ArgumentSyntaxException, ConnectionException {
URI interfaceURI = null;
URI deviceURI = null;
boolean isKNXIP;
try {
String[] deviceAddressSubStrings = deviceAddress.split(";");
if (deviceAddressSubStrings.length == 2) {
interfaceURI = new URI(deviceAddressSubStrings[0]);
deviceURI = new URI(deviceAddressSubStrings[1]);
isKNXIP = true;
}
else {
deviceURI = new URI(deviceAddress);
isKNXIP = false;
}
} catch (URISyntaxException e) {
logger.error("wrong format of interface address in deviceAddress");
throw new ArgumentSyntaxException();
}
IndividualAddress address = new IndividualAddress(0);
byte[] serialNumber = new byte[6];
if (settings != null) {
String[] settingsArray = settings.split(";");
for (String arg : settingsArray) {
int p = arg.indexOf("=");
if (p != -1) {
String key = arg.substring(0, p).toLowerCase().trim();
String value = arg.substring(p + 1).trim();
if (key.equalsIgnoreCase("address")) {
try {
address = new IndividualAddress(value);
logger.debug("setting individual address to " + address);
} catch (KNXFormatException e) {
logger.warn("wrong format of individual address in settings");
}
}
else if (key.equalsIgnoreCase("serialnumber")) {
if (value.length() == 12) {
value = value.toLowerCase();
for (int i = 0; i < 6; i++) {
String hexValue = value.substring(i * 2, (i * 2) + 2);
serialNumber[i] = (byte) Integer.parseInt(hexValue, 16);
}
logger.debug("setting serial number to " + DataUnitBuilder.toHex(serialNumber, ":"));
}
}
}
}
}
if (isKNXIP && isSchemeOk(deviceURI, KnxDriver.ADDRESS_SCHEME_KNXIP)
&& isSchemeOk(interfaceURI, KnxDriver.ADDRESS_SCHEME_KNXIP)) {
name = interfaceURI.getHost() + " - " + deviceURI.getHost();
logger.debug("connecting over KNX/IP from " + name.replace("-", "to"));
connectNetIP(interfaceURI, deviceURI, address);
}
else {
logger.error("wrong format of device URI in deviceAddress");
throw new ArgumentSyntaxException();
}
try {
processCommunicator = new ProcessCommunicatorImpl(knxNetworkLink);
processListener = new KnxProcessListener();
processCommunicator.addProcessListener(processListener);
setResponseTimeout(timeout);
} catch (KNXLinkClosedException e) {
e.printStackTrace();
throw new ConnectionException(e);
}
}
private boolean isSchemeOk(URI uri, String scheme) {
boolean isSchemeOK = uri.getScheme().toLowerCase().equals(scheme);
if (!isSchemeOK) {
logger.error("Scheme is not OK. Is " + uri.getScheme() + " should be " + scheme);
}
return isSchemeOK;
}
private void connectNetIP(URI localUri, URI remoteUri, IndividualAddress address) throws ConnectionException {
try {
String localIP = localUri.getHost();
int localPort = localUri.getPort() < 0 ? DEFAULT_PORT : localUri.getPort();
String remoteIP = remoteUri.getHost();
int remotePort = remoteUri.getPort() < 0 ? DEFAULT_PORT : remoteUri.getPort();
int serviceMode = KNXNetworkLinkIP.TUNNELING;
InetSocketAddress localSocket = new InetSocketAddress(localIP, localPort);
InetSocketAddress remoteSocket = new InetSocketAddress(remoteIP, remotePort);
boolean useNAT = true;
KNXMediumSettings settings = new TPSettings(address, true);
knxNetworkLink = new KNXNetworkLinkIP(serviceMode, localSocket, remoteSocket, useNAT, settings);
} catch (KNXException e) {
logger.error("Connection failed: " + e.getMessage());
throw new ConnectionException(e);
} catch (InterruptedException e) {
e.printStackTrace();
throw new ConnectionException(e);
}
}
private List<ChannelScanInfo> listKnownChannels() {
List<ChannelScanInfo> informations = new ArrayList<>();
Map<GroupAddress, byte[]> values = processListener.getCachedValues();
Set<GroupAddress> keys = values.keySet();
for (GroupAddress groupAddress : keys) {
byte[] asdu = values.get(groupAddress);
StringBuilder channelAddress = new StringBuilder();
channelAddress.append(groupAddress.toString()).append(":1.001");
StringBuilder description = new StringBuilder();
description.append("Datapoint length: ").append(asdu.length);
description.append("; Last datapoint ASDU: ").append(DataUnitBuilder.toHex(asdu, ":"));
informations.add(new ChannelScanInfo(channelAddress.toString(), description.toString(), null, null));
}
return informations;
}
private void ensureOpenConnection() throws ConnectionException {
if (!knxNetworkLink.isOpen())
throw new ConnectionException();
}
private Record read(KnxGroupDP groupDP, int timeout) throws ConnectionException, KNXException {
ensureOpenConnection();
Record record = null;
setResponseTimeout(timeout);
try {
groupDP.getKnxValue().setDPTValue(processCommunicator.read(groupDP));
record = new Record(groupDP.getKnxValue().getOpenMucValue(), System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
throw new ConnectionException("Read failed for group address " + groupDP.getMainAddress());
} catch (final KNXLinkClosedException e) {
throw new ConnectionException(e);
}
return record;
}
public boolean write(KnxGroupDP groupDP, int timeout) throws ConnectionException {
ensureOpenConnection();
setResponseTimeout(timeout);
try {
KnxValue value = groupDP.getKnxValue();
processCommunicator.write(groupDP, value.getDPTValue());
return true;
} catch (final KNXLinkClosedException e) {
throw new ConnectionException(e);
} catch (KNXException e) {
logger.warn("write failed");
return false;
}
}
private void setResponseTimeout(int timeout) {
if (responseTimeout != timeout) {
responseTimeout = timeout;
int timeoutSec = (timeout / 1000);
if (timeoutSec > 0) {
processCommunicator.setResponseTimeout(timeoutSec);
}
else {
processCommunicator.setResponseTimeout(DEFAULT_TIMEOUT);
}
}
}
@Override
public List<ChannelScanInfo> scanForChannels(String settings)
throws UnsupportedOperationException, ConnectionException {
return listKnownChannels();
}
@Override
public void disconnect() {
logger.debug("disconnecting from " + name);
processCommunicator.detach();
knxNetworkLink.close();
}
@Override
public Object read(List<ChannelRecordContainer> containers, Object containerListHandle, String samplingGroup)
throws UnsupportedOperationException, ConnectionException {
for (ChannelRecordContainer container : containers) {
try {
KnxGroupDP groupDP = null;
if (container.getChannelHandle() == null) {
groupDP = createKnxGroupDP(container.getChannelAddress());
logger.debug("New datapoint: " + groupDP);
container.setChannelHandle(groupDP);
}
else {
groupDP = (KnxGroupDP) container.getChannelHandle();
}
Record record = read(groupDP, KnxDriver.timeout);
container.setRecord(record);
} catch (ArgumentSyntaxException e) {
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_ADDRESS_SYNTAX_INVALID));
logger.error(e.getMessage() + "Channel-ID: " + container.getChannel().getId());
} catch (KNXTimeoutException e1) {
logger.debug(e1.getMessage());
container.setRecord(new Record(null, System.currentTimeMillis(), Flag.TIMEOUT));
} catch (KNXException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public void startListening(List<ChannelRecordContainer> containers, RecordsReceivedListener listener)
throws UnsupportedOperationException, ConnectionException {
for (ChannelRecordContainer container : containers) {
if (container.getChannelHandle() == null) {
try {
container.setChannelHandle(createKnxGroupDP(container.getChannelAddress()));
} catch (ArgumentSyntaxException e) {
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_ADDRESS_SYNTAX_INVALID));
logger.error(e.getMessage() + "Channel-ID: " + container.getChannel().getId());
} catch (KNXException e) {
e.printStackTrace();
}
}
}
logger.info("Start listening for " + containers.size() + " channels");
processListener.registerOpenMucListener(containers, listener);
}
@Override
public Object write(List<ChannelValueContainer> containers, Object containerListHandle)
throws UnsupportedOperationException, ConnectionException {
for (ChannelValueContainer container : containers) {
KnxGroupDP groupDP = null;
try {
if (container.getChannelHandle() == null) {
groupDP = createKnxGroupDP(container.getChannelAddress());
logger.debug("New datapoint: " + groupDP);
container.setChannelHandle(groupDP);
}
else {
groupDP = (KnxGroupDP) container.getChannelHandle();
}
groupDP.getKnxValue().setOpenMucValue(container.getValue());
boolean state = write(groupDP, KnxDriver.timeout);
if (state) {
container.setFlag(Flag.VALID);
}
else {
container.setFlag(Flag.UNKNOWN_ERROR);
}
} catch (ArgumentSyntaxException e) {
container.setFlag(Flag.DRIVER_ERROR_CHANNEL_ADDRESS_SYNTAX_INVALID);
logger.error(e.getMessage());
} catch (KNXException e) {
logger.warn(e.getMessage());
}
}
return null;
}
private static KnxGroupDP createKnxGroupDP(String channelAddress) throws KNXException, ArgumentSyntaxException {
String[] address = channelAddress.split(":");
KnxGroupDP dp = null;
if (address.length < 2 || address.length == 3 || address.length > 4) {
throw new ArgumentSyntaxException("Channel address has a wrong format. ");
}
else {
GroupAddress main = new GroupAddress(address[0]);
String dptID = address[1];
dp = new KnxGroupDP(main, channelAddress, dptID);
if (address.length == 4) {
boolean AET = address[2].equals("1");
String value = address[3];
}
}
return dp;
}
}