/**
* 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.tcp.protocol.internal;
import static org.apache.commons.lang.StringUtils.*;
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.StringEscapeUtils;
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.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 = "";
// 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);
}
}
protected void addBindingProvider(TCPBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(TCPBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
super.updated(config);
if (config != null) {
String timeOutString = (String) config.get("timeout");
if (isNotBlank(timeOutString)) {
timeOut = Integer.parseInt((timeOutString));
} else {
logger.info("The maximum time out for blocking write operations will be set to the default value of {}",
timeOut);
}
String blockingString = (String) config.get("blocking");
if (isNotBlank(blockingString)) {
blocking = Boolean.parseBoolean((blockingString));
} else {
logger.info("The blocking nature of read/write operations will be set to the default value of {}",
blocking);
}
String preambleString = (String) config.get("preamble");
if (isNotBlank(preambleString)) {
preAmble = StringEscapeUtils.unescapeJava(preambleString);
} else {
logger.info("The preamble for all write operations will be set to the default value of \"{}\"",
preAmble);
}
String postambleString = (String) config.get("postamble");
if (isNotBlank(postambleString)) {
postAmble = StringEscapeUtils.unescapeJava(postambleString);
} else {
logger.info("The postamble for all write operations will be set to the default value of \"{}\"",
postAmble);
}
String updatewithresponseString = (String) config.get("updatewithresponse");
if (isNotBlank(updatewithresponseString)) {
updateWithResponse = Boolean.parseBoolean((updatewithresponseString));
} else {
logger.info("Updating states with returned values will be set to the default value of {}",
updateWithResponse);
}
String charsetString = (String) config.get("charset");
if (isNotBlank(charsetString)) {
charset = charsetString;
} else {
logger.info("The characterset will be set to the default value of {}", charset);
}
}
}
@Override
protected void configureChannel(Channel channel) {
}
protected String transformResponse(String transformation, String response) {
String transformedResponse;
if (isEmpty(transformation) || transformation.equalsIgnoreCase("default")) {
transformedResponse = response;
} else {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
if (matcher.matches()) {
matcher.reset();
matcher.find();
String transformationServiceName = matcher.group(1);
String transformationServiceParam = matcher.group(2);
try {
TransformationService transformationService = TransformationHelper
.getTransformationService(TCPActivator.getContext(), transformationServiceName);
if (transformationService != null) {
transformedResponse = transformationService.transform(transformationServiceParam, response);
} else {
transformedResponse = response;
logger.warn(
"couldn't transform response because transformationService of type '{}' is unavailable",
transformationServiceName);
}
} catch (Exception te) {
logger.error("transformation throws exception [transformation={}, response={}]", transformation,
response, te);
// in case of an error we return the response without any
// transformation
transformedResponse = response;
}
} else {
transformedResponse = transformation;
}
}
logger.debug("transformed response is '{}'", transformedResponse);
return transformedResponse;
}
/**
* @{inheritDoc}
*/
@Override
protected String getName() {
return "TCP Refresh Service";
}
}