/**
* Copyright (c) 2010-2017 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.nio.channels.DatagramChannel;
import java.util.Dictionary;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.tcp.AbstractDatagramChannelBinding;
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.UDPBindingProvider;
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;
/**
* UDPBinding is an implementation of a UDP based ASCII protocol. It sends and receives
* 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 UDPBinding extends AbstractDatagramChannelBinding<UDPBindingProvider> implements ManagedService {
static private final Logger logger = LoggerFactory.getLogger(UDPBinding.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 receiving 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) {
return false;
}
String transformedMessage = transformResponse(provider.getProtocolCommand(itemName, command), commandAsString);
String UDPCommandName = preAmble + transformedMessage + postAmble;
ByteBuffer outputBuffer = null;
try {
outputBuffer = ByteBuffer.allocate(UDPCommandName.getBytes(charset).length);
outputBuffer.put(UDPCommandName.getBytes(charset));
} catch (UnsupportedEncodingException e) {
logger.warn("Data for output buffer appears to be using an unsupported encoding");
}
// send the buffer in an asynchronous way
ByteBuffer result = null;
try {
result = writeBuffer(outputBuffer, sChannel, blocking, timeOut);
} catch (Exception e) {
logger.warn("An exception occurred while writing a buffer to a channel", e);
}
if (result != null && blocking) {
String resultString = "";
try {
resultString = new String(result.array(), charset);
} catch (UnsupportedEncodingException e) {
logger.warn("Data received from write operation appears to be using an unsupported encoding");
}
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 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("Cannot parse transformed input {} to match command {} on item {}",
transformedResponse, command, itemName);
}
return false;
} else {
return true;
}
} else {
return true;
}
}
/**
*
* 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);
logger.debug("parseBuffer constructed update: '{}'", theUpdate);
} catch (UnsupportedEncodingException e) {
logger.warn("Exception while attempting an unsupported encoding scheme");
}
ProtocolBindingProvider provider = findFirstMatchingBindingProvider(itemName);
if (provider == null) {
logger.warn("No provider could be found for the item '{}'", itemName);
return;
}
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("Cannot parse input {} to match command {} on item {}", theUpdate, aCommand, itemName);
}
}
protected void addBindingProvider(UDPBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(UDPBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
super.updated(config);
if (config == null) {
return;
}
String timeOutString = Objects.toString(config.get("buffersize"), null);
if (isNotBlank(timeOutString)) {
timeOut = Integer.parseInt(timeOutString);
} else {
logger.info("The maximum timeout for blocking write operations will be set to the default value of {}",
timeOut);
}
String blockingString = Objects.toString(config.get("retryinterval"), null);
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 = Objects.toString(config.get("preamble"), null);
if (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 value of {}", preAmble);
}
String postambleString = Objects.toString(config.get("postamble"), null);
if (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 value of {}", postAmble);
}
String updateWithResponseString = Objects.toString(config.get("updatewithresponse"), null);
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 = Objects.toString(config.get("charset"), null);
if (isNotBlank(charsetString)) {
charset = charsetString;
} else {
logger.info("The character set will be set to the default value of {}", charset);
}
}
@Override
protected void configureChannel(DatagramChannel channel) {
}
protected String transformResponse(String transformation, String response) {
String transformedResponse;
if (isEmpty(transformation) || transformation.equalsIgnoreCase("default")) {
logger.debug("transformed response is '{}'", response);
return response;
}
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.warn("Transformation threw an 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 "UDP Refresh Service";
}
}