/**
* 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.davis.internal;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.davis.DavisBindingProvider;
import org.openhab.binding.davis.datatypes.DavisCommandType;
import org.openhab.binding.davis.datatypes.DavisValueType;
import org.openhab.core.binding.AbstractActiveBinding;
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;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
/**
* Binding for acquiring data from Davis Weather stations, e.g. Vantage Pro 2
*
* @author Trathnigg Thomas
* @since 1.6.0
*/
public class DavisBinding extends AbstractActiveBinding<DavisBindingProvider>implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(DavisBinding.class);
/**
* the refresh interval which is used to poll values from the Davis
* weather station (optional, defaults to 10000ms)
*/
private long refreshInterval = 10000;
private static final int BUF_LENGTH = 256;
private String port;
private SerialPort serialPort;
private InputStream inputStream;
private OutputStream outputStream;
public DavisBinding() {
}
@Override
public void activate() {
}
@Override
public void deactivate() {
closePort();
}
/**
* @{inheritDoc}
*/
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
/**
* @{inheritDoc}
*/
@Override
protected String getName() {
return "Davis Refresh Service";
}
/**
* @{inheritDoc}
*/
@Override
protected void execute() {
// the frequently executed code (polling) goes here ...
logger.trace("execute() method is called!");
try {
openPort();
// get all configured keys from providers, get needed read commands
// for them, send those read commands
for (DavisBindingProvider provider : providers) {
Collection<DavisCommand> commands = DavisValueType.getReadCommandsByKeys(provider.getConfiguredKeys());
for (DavisCommand command : commands) {
if (wakeup()) {
sendCommand(command);
} else {
logger.warn("Wakeup failed, trying reset sequence!");
resetAfterError();
}
}
}
closePort();
} catch (InitializationException e) {
logger.error(e.getMessage());
}
logger.trace("execute() method is finished!");
}
protected void addBindingProvider(DavisBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(DavisBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
logger.trace("update() method is called!");
if (config != null) {
// to override the default refresh interval one has to add a
// parameter to openhab.cfg like <bindingName>:refresh=<intervalInMs>
String refreshIntervalString = (String) config.get("refresh");
if (StringUtils.isNotBlank(refreshIntervalString)) {
refreshInterval = Long.parseLong(refreshIntervalString);
}
String newPort = (String) config.get("port"); //$NON-NLS-1$
if (StringUtils.isNotBlank(newPort) && !newPort.equals(port)) {
port = newPort;
setProperlyConfigured(true);
}
}
}
public void openPort() throws InitializationException {
CommPortIdentifier portIdentifier;
try {
portIdentifier = CommPortIdentifier.getPortIdentifier(port);
try {
serialPort = portIdentifier.open("openhab", 3000);
serialPort.setSerialPortParams(19200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
// serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
serialPort.enableReceiveTimeout(100);
serialPort.enableReceiveThreshold(1);
inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
outputStream = serialPort.getOutputStream();
logger.debug("port opened: " + port);
} catch (PortInUseException e) {
throw new InitializationException(e);
} catch (UnsupportedCommOperationException e) {
throw new InitializationException(e);
} catch (IOException e) {
throw new InitializationException(e);
}
} catch (NoSuchPortException e) {
StringBuilder sb = new StringBuilder();
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
sb.append(id.getName() + "\n");
}
}
throw new InitializationException(
"Serial port '" + port + "' could not be found. Available ports are:\n" + sb.toString());
}
}
public void closePort() {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
serialPort.close();
logger.debug("port closed: " + port);
}
protected void resetAfterError() {
// Drop the rest
try {
logger.warn("error, dropping remaining data!");
readResponse();
wakeup();
writeString("RXTEST\n");
byte[] buf = readResponse();
expectString(buf, "\n\rOK\n\r");
} catch (IOException e1) {
logger.warn("IO Exception reset after Error: " + e1);
closePort();
try {
openPort();
} catch (InitializationException e) {
logger.error("reopening port failed!");
}
}
}
public byte[] readResponse() throws IOException {
byte[] responseBlock = new byte[0];
byte[] readBuffer = new byte[BUF_LENGTH];
do {
while (inputStream.available() > 0) {
int bytes = inputStream.read(readBuffer);
// merge bytes
byte[] mergedBytes = new byte[responseBlock.length + bytes];
System.arraycopy(responseBlock, 0, mergedBytes, 0, responseBlock.length);
System.arraycopy(readBuffer, 0, mergedBytes, responseBlock.length, bytes);
responseBlock = mergedBytes;
}
try {
// add wait states around reading the stream, so that
// interrupted transmissions are merged
Thread.sleep(200);
} catch (InterruptedException e) {
// ignore interruption
}
} while (inputStream.available() > 0);
String string = new String(responseBlock);
logger.debug("RX: " + escape(string));
return responseBlock;
}
public void sendCommand(DavisCommand command) {
byte[] responseBlock;
int offset = 0;
logger.debug("sendCommand() method is called!");
DavisCommandType commandType = DavisCommandType.getCommandTypeByCommand(command.getRequestCmd());
try {
// command über rs232 schicken (inkl '\n'), wait for ACK
writeString(command.getRequestCmd() + '\n');
responseBlock = readResponse();
switch (commandType.getResponsetype()) {
case Constants.RESPONSE_TYPE_NONE:
break;
case Constants.RESPONSE_TYPE_ACK:
byte[] resp = new byte[1];
resp[0] = Constants.ACK;
expectString(responseBlock, new String(resp));
offset = 1;
break;
case Constants.RESPONSE_TYPE_OK:
expectString(responseBlock, "\n\rOK\n\r");
offset = 6;
break;
}
switch (commandType.getResponselimitertype()) {
case Constants.RESPONSE_LIMITER_TYPE_CRLF:
if (responseBlock[responseBlock.length - 2] != '\n'
|| responseBlock[responseBlock.length - 1] != '\r') {
throw new IOException("expected CRLF at end of response missing");
}
break;
case Constants.RESPONSE_LIMITER_TYPE_FIXED_SIZE:
if (responseBlock.length - offset != commandType.getResponselength()) {
throw new IOException("expected length of response: " + commandType.getResponselength()
+ ", but got: " + (responseBlock.length - offset));
}
break;
case Constants.RESPONSE_LIMITER_TYPE_MULTIPLE_CRLF:
// TODO add support
break;
}
switch (commandType.getCrcchecktype()) {
case Constants.CRC_CHECK_TYPE_VAR1:
if (!CRC16.check(responseBlock, offset, responseBlock.length - offset)) {
throw new IOException("CRC error");
}
break;
case Constants.CRC_CHECK_TYPE_NONE:
break;
}
try {
int responseLength = responseBlock.length - offset
- (commandType.getCrcchecktype() == Constants.CRC_CHECK_TYPE_VAR1 ? 2 : 0);
byte[] inputBuf = new byte[responseLength];
System.arraycopy(responseBlock, offset, inputBuf, 0, responseLength);
String string = new String(inputBuf);
logger.debug("parsing: " + escape(string));
// aus response alle werte dekodieren für es ein item gibt -> postUpdate
Set<DavisValueType> valueTypes = DavisValueType.getValueTypesByCommandType(commandType);
for (DavisValueType valueType : valueTypes) {
// get items and post Updates for all items with key
for (DavisBindingProvider provider : providers) {
List<String> itemNames = provider.getItemNamesForKey(valueType.getKey());
State state = valueType.getDataType().convertToState(inputBuf, valueType);
for (String itemName : itemNames) {
eventPublisher.postUpdate(itemName, state);
}
}
}
} catch (ArrayIndexOutOfBoundsException aie) {
logger.warn(aie.getMessage());
}
} catch (IOException e) {
logger.warn(e.getMessage());
resetAfterError();
}
}
protected void writeString(String string) throws IOException {
logger.debug("TX: " + escape(string));
outputStream.write(string.getBytes());
outputStream.flush();
}
protected void expectString(byte[] buffer, String string) throws IOException {
String s = new String(buffer);
if (buffer.length < string.length()) {
throw new IOException("unexpected response too short: " + escape(s) + ", expected: " + escape(string));
}
if (!string.equals(s.substring(0, string.length()))) {
throw new IOException("unexpected response mismatch: " + escape(s.substring(0, string.length()))
+ ", expected: " + escape(string));
}
}
protected boolean wakeup() {
// Send wakeup command.
boolean awake = false;
int i = 0;
while (awake == false && i++ < 3) {
try {
logger.debug("sending wakeup sequence");
writeString("\n");
sleep(100);
logger.debug("waiting for wakeup response");
byte[] buf = readResponse();
expectString(buf, "\n\r");
awake = true;
return awake;
} catch (IOException e) {
logger.warn("wakeup failed, retry");
}
sleep(1200);
}
return awake;
}
protected void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ex) {
logger.debug("sleep interrupted");
}
}
protected String escape(String string) {
byte[] bytes = string.getBytes();
return escape(bytes, 0, bytes.length, true);
}
protected String escape(byte[] bytes, int offset, int length, boolean printWritable) {
if (offset > length) {
throw new IllegalArgumentException("offset " + offset + " is greater than length " + length);
}
StringBuilder buf = new StringBuilder();
for (int i = offset; i < (offset + length); i++) {
switch (bytes[i]) {
case '\n':
buf.append(printWritable ? "\\n" : "<0x0a>");
break;
case '\r':
buf.append(printWritable ? "\\r" : "<0x0d>");
break;
case '\t':
buf.append(printWritable ? "\\t" : "<0x09>");
break;
case 0x06:
buf.append(printWritable ? "<ACK>" : "<0x06>");
break;
case 0x18:
buf.append(printWritable ? "<CAN>" : "<0x18>");
break;
case 0x21:
buf.append(printWritable ? "<NAK>" : "<0x21>");
break;
default:
if (bytes[i] < 0x20 || bytes[i] > 0x7e || !printWritable) {
String s = Integer.toHexString(bytes[i] & 0x000000ff);
buf.append("<0x");
if (s.length() == 1) {
buf.append('0');
}
buf.append(s);
buf.append('>');
} else {
buf.append((char) bytes[i]);
}
break;
}
}
return buf.toString();
}
}