/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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.tcp.protocol.internal;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Dictionary;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.tcp.AbstractSocketChannelBinding;
import org.openhab.binding.tcp.Direction;
import org.openhab.binding.tcp.internal.TCPActivator;
import org.openhab.binding.tcp.protocol.ProtocolBindingProvider;
import org.openhab.binding.tcp.protocol.TCPBindingProvider;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TCPBinding is most "simple" implementation of a TCP based ASCII protocol. It sends and received
* data as ASCII strings. Data sent out is padded with a CR/LF. This should be sufficient for a lot
* of home automation devices that take simple ASCII based control commands, or that send back
* text based status messages
*
*
* @author Karel Goderis
* @since 1.1.0
*
*/
public class TCPBinding extends AbstractSocketChannelBinding<TCPBindingProvider> implements ManagedService {
static private final Logger logger = LoggerFactory.getLogger(TCPBinding.class);
/** RegEx to extract a parse a function String <code>'(.*?)\((.*)\)'</code> */
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\)");
// time to wait for a reply, in milliseconds
private static int timeOut = 3000;
// flag to use only blocking write/read operations
private static boolean blocking = false;
// string to prepend to data being sent
private static String preAmble = "";
// string to append to data being sent
private static String postAmble = "\r\n";
// flag to use the reply of the remote end to update the status of the Item receving the data
private static boolean updateWithResponse = true;
// used character set
private static String charset = "ASCII";
@Override
protected boolean internalReceiveChanneledCommand(String itemName,
Command command, Channel sChannel, String commandAsString) {
ProtocolBindingProvider provider = findFirstMatchingBindingProvider(itemName);
if(command != null ){
String transformedMessage = transformResponse(provider.getProtocolCommand(itemName, command),commandAsString);
String tcpCommandName = preAmble + transformedMessage + postAmble ;
ByteBuffer outputBuffer = null;
try {
outputBuffer = ByteBuffer.allocate(tcpCommandName.getBytes(charset).length);
outputBuffer.put(tcpCommandName.getBytes(charset));
} catch (UnsupportedEncodingException e) {
logger.warn("Exception while attempting an unsupported encoding scheme");
}
// send the buffer in an asynchronous way
ByteBuffer result = null;
try {
result = writeBuffer(outputBuffer,sChannel,blocking,timeOut);
} catch (Exception e) {
logger.error("An exception occurred while writing a buffer to a channel: {}",e.getMessage());
}
if(result!=null && blocking) {
String resultString = "";
try {
resultString = new String(result.array(), charset).split("\0")[0];
} catch (UnsupportedEncodingException e) {
logger.warn("Exception while attempting an unsupported encoding scheme");
}
logger.info("Received {} from the remote end {}", resultString, sChannel.toString());
String transformedResponse = transformResponse(provider.getProtocolCommand(itemName, command), resultString);
// if the remote-end does not send a reply in response to the string we just sent, then the abstract superclass will update
// the openhab status of the item for us. If it does reply, then an additional update is done via parseBuffer.
// since this TCP binding does not know about the specific protocol, there might be two state updates (the command, and if
// the case, the reply from the remote-end)
if(updateWithResponse) {
List<Class<? extends State>> stateTypeList = provider.getAcceptedDataTypes(itemName,command);
State newState = createStateFromString(stateTypeList,transformedResponse);
if(newState != null) {
eventPublisher.postUpdate(itemName, newState);
} else {
logger.warn("Can not parse transformed output "+transformedResponse+" to match command {} on item {} ",command,itemName);
}
return false;
} else {
return true;
}
} else {
return true;
}
}
return false;
}
/**
*
* Main function to parse ASCII string received
* @return
*
*/
@Override
protected void parseBuffer(String itemName, Command aCommand, Direction theDirection,ByteBuffer byteBuffer){
String theUpdate = "";
try {
theUpdate = new String(byteBuffer.array(), charset).split("\0")[0];
} catch (UnsupportedEncodingException e) {
logger.warn("Exception while attempting an unsupported encoding scheme");
}
ProtocolBindingProvider provider = findFirstMatchingBindingProvider(itemName);
List<Class<? extends State>> stateTypeList = provider.getAcceptedDataTypes(itemName,aCommand);
String transformedResponse = transformResponse(provider.getProtocolCommand(itemName, aCommand),theUpdate);
State newState = createStateFromString(stateTypeList,transformedResponse);
if(newState != null) {
eventPublisher.postUpdate(itemName, newState);
} else {
logger.warn("Can not parse input "+theUpdate+" to match command {} on item {} ",aCommand,itemName);
}
}
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
super.updated(config);
if (config != null) {
String timeOutString = (String) config.get("timeout");
if (StringUtils.isNotBlank(timeOutString)) {
timeOut = Integer.parseInt((timeOutString));
} else {
logger.info("The maximum time out for blocking write operations will be set to the default vaulue of {}",timeOut);
}
String blockingString = (String) config.get("blocking");
if (StringUtils.isNotBlank(blockingString)) {
blocking = Boolean.parseBoolean((blockingString));
} else {
logger.info("The blocking nature of read/write operations will be set to the default vaulue of {}",blocking);
}
String preambleString = (String) config.get("preamble");
if (StringUtils.isNotBlank(preambleString)) {
try {
preAmble = preambleString.replaceAll("\\\\", "\\");
}
catch(Exception e) {
preAmble = preambleString;
}
} else {
logger.info("The preamble for all write operations will be set to the default vaulue of {}",preAmble);
}
String postambleString = (String) config.get("postamble");
if (StringUtils.isNotBlank(postambleString)) {
try {
postAmble = postambleString.replaceAll("\\\\", "\\");
}
catch(Exception e) {
postAmble = postambleString;
}
} else {
logger.info("The postamble for all write operations will be set to the default vaulue of {}",postAmble);
}
String updatewithresponseString = (String) config.get("updatewithresponse");
if (StringUtils.isNotBlank(updatewithresponseString)) {
updateWithResponse = Boolean.parseBoolean((updatewithresponseString));
} else {
logger.info("Updating states with returned values will be set to the default vaulue of {}",updateWithResponse);
}
String charsetString = (String) config.get("charset");
if (StringUtils.isNotBlank(charsetString)) {
charset = charsetString;
} else {
logger.info("The characterset will be set to the default vaulue of {}",charset);
}
}
}
@Override
protected void configureChannel(Channel channel) {
}
/**
* Splits a transformation configuration string into its two parts - the
* transformation type and the function/pattern to apply.
*
* @param transformation the string to split
* @return a string array with exactly two entries for the type and the function
*/
protected String[] splitTransformationConfig(String transformation) {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
if (!matcher.matches()) {
throw new IllegalArgumentException("given transformation function '" + transformation + "' does not follow the expected pattern '<function>(<pattern>)'");
}
matcher.reset();
matcher.find();
String type = matcher.group(1);
String pattern = matcher.group(2);
return new String[] { type, pattern };
}
protected String transformResponse(String transformation, String response) {
String transformedResponse;
try {
String[] parts = splitTransformationConfig(transformation);
String transformationType = parts[0];
String transformationFunction = parts[1];
TransformationService transformationService =
TransformationHelper.getTransformationService(TCPActivator.getContext(), transformationType);
if (transformationService != null) {
transformedResponse = transformationService.transform(transformationFunction, response);
} else {
transformedResponse = response;
logger.warn("couldn't transform response because transformationService of type '{}' is unavailable", transformationType);
}
}
catch (Exception te) {
logger.error("transformation throws exception [transformation="
+ transformation + ", response=" + response + "]", te);
// in case of an error we return the response without any
// transformation
transformedResponse = response;
}
logger.debug("transformed response is '{}'", transformedResponse);
return transformedResponse;
}
/**
* @{inheritDoc}
*/
@Override
protected String getName() {
return "TCP Refresh Service";
}
}