/**
* 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.oceanic.internal;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
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.concurrent.locks.ReentrantLock;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.oceanic.OceanicBindingProvider;
import org.openhab.binding.oceanic.OceanicValueSelector;
import org.openhab.binding.oceanic.OceanicValueSelector.ValueSelectorType;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.types.Command;
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.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
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 Oceanic Watersoftener.
*
* @author Karel Goderis
* @since 1.5.0
*
*/
public class OceanicBinding extends AbstractActiveBinding<OceanicBindingProvider>implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(OceanicBinding.class);
/** stores information about serial devices / pump gateways in use */
private Map<String, SerialDevice> serialDevices = new HashMap<String, SerialDevice>();
private ReentrantLock serialDevicesLock = new ReentrantLock();
/**
* 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.
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
OceanicBindingProvider provider = findFirstMatchingBindingProvider(itemName);
String serialPort = provider.getSerialPort(itemName);
OceanicValueSelector valueSelector = OceanicValueSelector.getValueSelector(provider.getValueSelector(itemName),
ValueSelectorType.SET);
SerialDevice serialDevice = serialDevices.get(serialPort);
if (valueSelector.name().contains("set")) {
String commandAsString = command.toString();
switch (valueSelector) {
case setSV1:
commandAsString = valueSelector.name() + commandAsString;
default:
commandAsString = valueSelector.name();
break;
}
String response = serialDevice.requestResponse(commandAsString);
if (response.equals("ERR")) {
logger.error("An error occurred while setting '{}' to {}", valueSelector.toString(),
command.toString());
}
} else {
// can not set the value of a read-only "get" variable
logger.warn("An error occurred while trying to set the read-only variable '{}' to {}",
valueSelector.toString(), command.toString());
}
}
protected void addBindingProvider(OceanicBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(OceanicBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@SuppressWarnings("rawtypes")
public void updated(Dictionary config) throws ConfigurationException {
setProperlyConfigured(true);
}
@Override
protected void execute() {
// TODO Change binding to AbstractActive type and move code in execute() to
// TODO bindingChanged()
if (isProperlyConfigured()) {
Scheduler sched = null;
try {
sched = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
logger.error("An exception occurred while getting a reference to the Quartz Scheduler");
}
// reset the contextMap before rebuilding it
for (String serialPort : serialDevices.keySet()) {
Set<String> itemNames = contextMap.get(serialPort);
if (itemNames != null) {
contextMap.clear();
}
}
for (OceanicBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
String serialPort = provider.getSerialPort(itemName);
SerialDevice serialDevice = serialDevices.get(serialPort);
boolean serialDeviceReady = true;
if (serialDevice == null) {
serialDevice = new SerialDevice(serialPort);
try {
serialDevice.initialize();
} catch (InitializationException e) {
logger.error("Could not open serial port " + serialPort + ": " + e.getMessage());
serialDeviceReady = false;
} catch (Throwable e) {
logger.error("Could not open serial port " + serialPort + ": " + e.getMessage());
serialDeviceReady = false;
}
if (serialDeviceReady) {
serialDevice.setEventPublisher(eventPublisher);
serialDevices.put(serialPort, serialDevice);
}
}
Set<String> itemNames = contextMap.get(serialPort);
if (itemNames == null) {
itemNames = new HashSet<String>();
contextMap.put(serialPort, itemNames);
}
itemNames.add(itemName);
if (serialDeviceReady) {
// set up the polling jobs
boolean jobExists = false;
// enumerate each job group
try {
for (String group : sched.getJobGroupNames()) {
// enumerate each job in group
if (group.equals("Oceanic-" + provider.toString())) {
for (JobKey jobKey : sched.getJobKeys(jobGroupEquals(group))) {
if (jobKey.getName().equals(
itemName + "-" + provider.getValueSelector(itemName).toString())) {
jobExists = true;
break;
}
}
}
}
} catch (SchedulerException e1) {
logger.error("An exception occurred while querying the Quartz Scheduler ({})",
e1.getMessage());
}
if (!jobExists && OceanicValueSelector.getValueSelector(provider.getValueSelector(itemName),
ValueSelectorType.GET) != null) {
// set up the Quartz jobs
JobDataMap map = new JobDataMap();
map.put("SerialPort", serialPort);
map.put("ValueSelector", OceanicValueSelector
.getValueSelector(provider.getValueSelector(itemName), ValueSelectorType.GET));
map.put("Binding", this);
JobDetail job = newJob(OceanicBinding.PollJob.class)
.withIdentity(itemName + "-" + provider.getValueSelector(itemName).toString(),
"Oceanic-" + provider.toString())
.usingJobData(map).build();
Trigger trigger = newTrigger()
.withIdentity(itemName + "-" + provider.getValueSelector(itemName).toString(),
"Oceanic-" + provider.toString())
.startNow().withSchedule(simpleSchedule().repeatForever()
.withIntervalInSeconds(provider.getPollingInterval(itemName)))
.build();
try {
logger.debug("Adding a poll job {} for {}", job.getKey(), itemName);
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("An exception occurred while scheduling a Quartz Job");
}
}
// kill the Quartz jobs that we do not need anymore
try {
for (String group : sched.getJobGroupNames()) {
// enumerate each job in group
if (group.equals("Oceanic-" + provider.toString())) {
for (JobKey jobKey : sched.getJobKeys(jobGroupEquals(group))) {
if (findFirstMatchingBindingProvider(jobKey.getName().split("-")[0]) == null) {
logger.debug("Removing a poll job {} for {}", jobKey, itemName);
sched.deleteJob(jobKey);
}
}
}
}
} catch (SchedulerException e1) {
logger.error("An exception occurred while querying the Quartz Scheduler ({})",
e1.getMessage());
}
}
}
}
// 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);
logger.debug("Closing the serial port {}", serialPort);
serialDevice.close();
serialDevices.remove(serialPort);
}
}
}
}
protected OceanicBindingProvider findFirstMatchingBindingProvider(String itemName) {
OceanicBindingProvider firstMatchingProvider = null;
for (OceanicBindingProvider provider : providers) {
if (provider.providesBindingFor(itemName)) {
firstMatchingProvider = provider;
break;
}
}
return firstMatchingProvider;
}
public void lockSerialDevices() {
serialDevicesLock.lock();
}
public void unlockSerialDevices() {
serialDevicesLock.unlock();
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "Oceanic Refresh Service";
}
protected class SerialDevice implements SerialPortEventListener {
private String port;
private int baud = 19200;
/**
* we store the previous value of a status variable, and only publish Updates on the bus
* if the value differs.
*/
private HashMap<OceanicValueSelector, String> cachedValues = new HashMap<OceanicValueSelector, String>();
private EventPublisher eventPublisher;
private CommPortIdentifier portId;
private SerialPort serialPort;
private InputStream inputStream;
private OutputStream outputStream;
private String response = "";
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;
}
public void clearResponse() {
response = "";
}
public String getResponse() {
return response;
}
/**
* 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);
if (br.ready()) {
String line = br.readLine();
line = StringUtils.chomp(line);
line = line.replace(",", ".");
response = line.trim();
}
} catch (IOException e) {
logger.debug("Error receiving data on serial port {}: {}",
new Object[] { port, e.getMessage() });
}
break;
}
}
/**
* Sends a string to the serial port of this device
*
* @param msg the string to send
*/
public void writeString(String msg) {
try {
outputStream.write(msg.getBytes());
outputStream.flush();
} catch (IOException e) {
logger.error("Error writing '{}' to serial port {}: {}", new Object[] { msg, port, e.getMessage() });
}
}
public String requestResponse(String msg) {
synchronized (this) {
writeString(msg + "\r");
String response = "";
while (response.equals("")) {
response = getResponse();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
logger.debug("An exception occurred while putting the thread to sleep: {}", e.getMessage());
e.printStackTrace();
}
}
clearResponse();
return response;
}
}
/**
* Close this serial device
*/
public void close() {
serialPort.removeEventListener();
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
serialPort.close();
}
}
public static class PollJob implements Job {
@SuppressWarnings("unchecked")
private State createStateForType(OceanicValueSelector 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, selector.convertValue(value));
return state;
}
public void execute(JobExecutionContext context) throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String serialPort = (String) dataMap.get("SerialPort");
OceanicValueSelector valueSelector = (OceanicValueSelector) dataMap.get("ValueSelector");
OceanicBinding theBinding = (OceanicBinding) dataMap.get("Binding");
theBinding.lockSerialDevices();
SerialDevice serialDevice = theBinding.serialDevices.get(serialPort);
String response = null;
if (serialDevice != null) {
response = serialDevice.requestResponse(valueSelector.name());
logger.debug("Requested '{}' from the oceanic unit, got '{}' back", valueSelector.name(), response);
}
theBinding.unlockSerialDevices();
// process response etc
if (response != null) {
for (OceanicBindingProvider provider : theBinding.providers) {
for (String itemName : provider.getItemNames()) {
String itemSerialPort = provider.getSerialPort(itemName);
OceanicValueSelector itemSelector = OceanicValueSelector
.getValueSelector(provider.getValueSelector(itemName), ValueSelectorType.GET);
if (itemSerialPort.equals(serialPort) && itemSelector.equals(valueSelector)) {
if (serialDevice.cachedValues.get(valueSelector) == null
|| !serialDevice.cachedValues.get(valueSelector).equals(response)) {
serialDevice.cachedValues.put(valueSelector, response);
State value;
try {
value = createStateForType(valueSelector, response);
} catch (BindingConfigParseException e) {
logger.error("An exception occured while converting {} to a valid state : {}",
response, e.getMessage());
return;
}
serialDevice.eventPublisher.postUpdate(itemName, value);
}
}
}
}
}
}
}
}