/**
* 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.rme.internal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TooManyListenersException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.rme.RMEBindingProvider;
import org.openhab.binding.rme.RMEValueSelector;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.TypeParser;
import org.openhab.model.item.binding.BindingConfigParseException;
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.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import gnu.io.UnsupportedCommOperationException;
/**
*
* Binding to support the RME Rain Manager. The RME is a rain water management system that is
* sold by GEP (www.regenwater.be or regenwater.nl or www.dehoust.de). The serial gateway that the binding
* is interfacing to also supports pump units from Konsole, Monsun, and Grundfos. The format of the
* data emitted by the serial interface is set by setting dip switched on the gateway; the binding
* currently supports the GEP "RME" but can be modified easily to support other kinds of pumps
*
* @author Karel Goderis
* @since 1.5.0
*
*/
public class RMEBinding extends AbstractActiveBinding<RMEBindingProvider>implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(RMEBinding.class);
/** stores information about serial devices / pump gateways in use */
private Map<String, SerialDevice> serialDevices = new HashMap<String, SerialDevice>();
/**
* stores information about the which items are associated to which port. The map has this content structure:
* itemname -> port
*/
private Map<String, String> itemMap = new HashMap<String, String>();
/**
* stores information about the context of items. The map has this content structure: context -> Set of itemNames
*/
private Map<String, Set<String>> contextMap = new HashMap<String, Set<String>>();
/** the refresh interval which is used to check for changes in the binding configurations */
private static long refreshInterval = 5000;
@Override
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
for (SerialDevice serialDevice : serialDevices.values()) {
serialDevice.setEventPublisher(eventPublisher);
}
}
@Override
public void unsetEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = null;
for (SerialDevice serialDevice : serialDevices.values()) {
serialDevice.setEventPublisher(null);
}
}
@Override
public void activate() {
// Nothing to do here.
}
protected void addBindingProvider(RMEBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(RMEBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@SuppressWarnings("rawtypes")
public void updated(Dictionary config) throws ConfigurationException {
setProperlyConfigured(true);
}
@Override
protected void execute() {
if (isProperlyConfigured()) {
for (RMEBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
String serialPort = provider.getSerialPort(itemName);
SerialDevice serialDevice = serialDevices.get(serialPort);
if (serialDevice == null) {
serialDevice = new SerialDevice(serialPort);
serialDevice.setEventPublisher(eventPublisher);
try {
serialDevice.initialize();
} catch (InitializationException e) {
logger.error("Could not open serial port " + serialPort + ": " + e.getMessage());
} catch (Throwable e) {
logger.error("Could not open serial port " + serialPort + ": " + e.getMessage());
}
itemMap.put(itemName, serialPort);
serialDevices.put(serialPort, serialDevice);
}
Set<String> itemNames = contextMap.get(serialPort);
if (itemNames == null) {
itemNames = new HashSet<String>();
contextMap.put(serialPort, itemNames);
}
itemNames.add(itemName);
}
}
// close down the serial ports that do not have any Items anymore associated to them
for (String serialPort : serialDevices.keySet()) {
SerialDevice serialDevice = serialDevices.get(serialPort);
Set<String> itemNames = contextMap.get(serialPort);
if (itemNames == null || itemNames.size() == 0) {
contextMap.remove(serialPort);
serialDevice.close();
serialDevices.remove(serialPort);
}
}
}
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "RME Refresh Service";
}
protected class SerialDevice implements SerialPortEventListener {
private String port;
private int baud = 2400;
private String previousLine = null;
/**
* we store the previous value of a status variable, and only publish Updates on the bus
* if the value differs.
*/
private HashMap<RMEValueSelector, String> cachedValues = new HashMap<RMEValueSelector, String>();
private EventPublisher eventPublisher;
private CommPortIdentifier portId;
private SerialPort serialPort;
private InputStream inputStream;
private OutputStream outputStream;
public SerialDevice(String port) {
this.port = port;
}
public SerialDevice(String port, int baud) {
this.port = port;
this.baud = baud;
}
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void unsetEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = null;
}
public String getPort() {
return port;
}
/**
* Initialize this device and open the serial port
*
* @throws InitializationException if port can not be opened
*/
@SuppressWarnings("rawtypes")
public void initialize() throws InitializationException {
// parse ports and if the default port is found, initialized the reader
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
if (id.getName().equals(port)) {
logger.debug("Serial port '{}' has been found.", port);
portId = id;
}
}
}
if (portId != null) {
// initialize serial port
try {
serialPort = portId.open("openHAB", 2000);
} catch (PortInUseException e) {
throw new InitializationException(e);
}
try {
inputStream = serialPort.getInputStream();
} catch (IOException e) {
throw new InitializationException(e);
}
try {
serialPort.addEventListener(this);
} catch (TooManyListenersException e) {
throw new InitializationException(e);
}
// activate the DATA_AVAILABLE notifier
serialPort.notifyOnDataAvailable(true);
try {
// set port parameters
serialPort.setSerialPortParams(baud, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
} catch (UnsupportedCommOperationException e) {
throw new InitializationException(e);
}
try {
// get the output stream
outputStream = serialPort.getOutputStream();
} catch (IOException e) {
throw new InitializationException(e);
}
} else {
StringBuilder sb = new StringBuilder();
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 serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.BI:
case SerialPortEvent.OE:
case SerialPortEvent.FE:
case SerialPortEvent.PE:
case SerialPortEvent.CD:
case SerialPortEvent.CTS:
case SerialPortEvent.DSR:
case SerialPortEvent.RI:
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
break;
case SerialPortEvent.DATA_AVAILABLE:
try {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream), 32 * 1024 * 1024);
while (br.ready()) {
String line = br.readLine();
line = StringUtils.chomp(line);
// little hack to overcome Locale limits of the RME Rain Manager
// note to the attentive reader : should we add support for system locale's
// in the Type classes? ;-)
line = line.replace(",", ".");
line = line.trim();
if (previousLine == null) {
previousLine = line;
}
if (!previousLine.equals(line)) {
processData(line);
previousLine = line;
}
}
} catch (IOException e) {
logger.debug("Error receiving data on serial port {}: {}",
new Object[] { port, e.getMessage() });
}
break;
}
}
private void processData(String data) {
if (data != null) {
Pattern RESPONSE_PATTERN = Pattern
.compile("(.*);(0|1);(0|1);(0|1);(0|1);(0|1);(0|1);(0|1);(0|1);(0|1)");
Matcher matcher = RESPONSE_PATTERN.matcher(data);
if (matcher.matches()) {
for (RMEBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
String serialPort = provider.getSerialPort(itemName);
RMEValueSelector selector = provider.getValueSelector(itemName);
if (port.equals(serialPort)) {
if (cachedValues.get(selector) == null || !cachedValues.get(selector)
.equals(matcher.group(selector.getFieldIndex()))) {
cachedValues.put(selector, matcher.group(selector.getFieldIndex()));
State value;
try {
if (matcher.group(selector.getFieldIndex()).equals("0")) {
value = createStateForType(selector, "OFF");
} else if (matcher.group(selector.getFieldIndex()).equals("1")) {
value = createStateForType(selector, "ON");
} else {
value = createStateForType(selector,
matcher.group(selector.getFieldIndex()));
}
} catch (BindingConfigParseException e) {
logger.error("An exception occured while converting {} to a valid state : {}",
matcher.group(selector.getFieldIndex()), e.getMessage());
return;
}
eventPublisher.postUpdate(itemName, value);
}
}
}
}
}
}
}
@SuppressWarnings("unchecked")
private State createStateForType(RMEValueSelector selector, String value) throws BindingConfigParseException {
Class<? extends Type> typeClass = selector.getTypeClass();
List<Class<? extends State>> stateTypeList = new ArrayList<Class<? extends State>>();
stateTypeList.add((Class<? extends State>) typeClass);
State state = TypeParser.parseState(stateTypeList, value);
return state;
}
/**
* Close this serial device
*/
public void close() {
serialPort.removeEventListener();
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
serialPort.close();
}
}
}