/**
* 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.dsmr.internal;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import org.openhab.binding.dsmr.internal.messages.OBISMessage;
import org.openhab.binding.dsmr.internal.p1telegram.P1TelegramParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
/**
* Class that implements the DSMR port for energy meters that comply to the
* Dutch Smart Meter Requirements.
* <p>
* This class provides a simple public interface: read and close.
* <p>
* The read method will claim OS resources if necessary. If the read method
* encounters problems it will automatically close itself
* <p>
* The close method can be called asynchronous and will release OS resources.
* <p>
* In this way the DSMR port can restore the connection automatically
* <p>
* <code>
* An example DSMR telegram in accordance to IEC 62056-21 Mode D.<br>
* /ISk5\2MT382-1000<br>
* 0-0:96.1.1(4B384547303034303436333935353037)<br>
* 1-0:1.8.1(12345.678*kWh)<br>
* 1-0:1.8.2(12345.678*kWh)<br>
* 1-0:2.8.1(12345.678*kWh)<br>
* 1-0:2.8.2(12345.678*kWh)<br>
* 0-0:96.14.0(0002)<br>
* 1-0:1.7.0(001.19*kW)<br>
* 1-0:2.7.0(000.00*kW)<br>
* 0-0:17.0.0(016*A)<br>
* 0-0:96.3.10(1)<br>
* 0-0:96.13.1(303132333435363738)<br>
* 0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F<br>
* 303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F<br>
* 303132333435363738393A3B3C3D3E3F)<br>
* 0-1:96.1.0(3232323241424344313233343536373839)<br>
* 0-1:24.1.0(03)<br>
* 0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)<br>
* (00000.000)<br>
* 0-1:24.4.0(1)<br>
* !<br>
* </code>
*
* @author M. Volaart
* @since 1.7.0
*/
public class DSMRPort {
/* logger */
private static final Logger logger = LoggerFactory.getLogger(DSMRPort.class);
private enum PortState {
CLOSED,
AUTO_DETECT,
OPENED;
}
/* private object variables */
private final String portName;
private final int readTimeoutMSec;
private final int autoDetectTimeoutMSec;
private long autoDetectTS;
/* serial port resources */
private SerialPort serialPort;
private BufferedInputStream bis;
private byte[] buffer = new byte[1024]; // 1K
/* state variables */
private PortState portState;
private DSMRPortSettings portSettings;
private DSMRPortSettings fixedPortSettings; // Used if DSMR binding has a static port configuration
/* helpers */
private P1TelegramParser p1Parser;
/*
* The portLock is used for the shared data used when opening and closing
* the port. The following shared data must be guarded by the lock:
* SerialPort, BufferedReader, isOpen
*/
private Object portLock = new Object();
/**
* Creates a new DSMRPort. This is only a reference to a port. The port will
* not be opened nor it is checked if the DSMR Port can successfully be
* opened.
*
* @param portName
* Device identifier of the post (e.g. /dev/ttyUSB0)
* @param p1Parser
* {@link P1TelegramParser}
* @param readTimeoutMSec
* communication timeout in milliseconds
* @param autoDetectTimeoutMSec
* timeout for auto detection in milliseconds (after this period
* the Serial Port speed will be changed)
* @param fixedPortSettings
* {@link PortSettings} object containing fixed port settings. This parameter
* may be null. The binding will then use specification default settings
* HIGH_SPEED (i.e. 115200 8N1) and LOW_SPEED (9600 7E1) and auto detect which
* is applicable.
* If the parameter is set, the binding will ONLY use the specified settings
* auto detect functionality will only use the specified settings.
*/
public DSMRPort(String portName, P1TelegramParser p1Parser, int readTimeoutMSec, int autoDetectTimeoutMSec,
DSMRPortSettings fixedPortSettings) {
this.portName = portName;
this.readTimeoutMSec = readTimeoutMSec;
this.autoDetectTimeoutMSec = autoDetectTimeoutMSec;
this.p1Parser = p1Parser;
this.fixedPortSettings = fixedPortSettings;
portSettings = DSMRPortSettings.HIGH_SPEED_SETTINGS;
portState = PortState.CLOSED;
}
/**
* Returns whether or not the port is open
*
* @return true if the DSMRPort is open, false otherwise
*/
public boolean isOpen() {
return portState != PortState.CLOSED;
}
/**
* Closes the DSMRPort and release OS resources
*/
public void close() {
synchronized (portLock) {
logger.info("Closing DSMR port");
portState = PortState.CLOSED;
// Close resources
if (bis != null) {
try {
bis.close();
} catch (IOException ioe) {
logger.debug("Failed to close reader", ioe);
}
}
if (serialPort != null) {
serialPort.close();
}
// Release resources
bis = null;
serialPort = null;
}
}
/**
* Reads a complete telegram from the DSMR port.
* <p>
* If the read is successful a list of received @{link OBISMessage} is
* returned. If the read encounters problems the port will be closed and a
* list of received {@link OBISMessage} is returned.
* <p>
* It is a technically valid that the read succeeds with an empty list. Most
* likely there is a configuration problem of the global DSMR binding
*
* @return List of {@link OBISMessage} with 0 or more entries
*/
public List<OBISMessage> read() {
List<OBISMessage> receivedMessages = new LinkedList<OBISMessage>();
handlePortState();
// open port if it is not open
if (portState == PortState.CLOSED) {
logger.warn("Could not open DSMRPort, no values will be read");
close();
return receivedMessages;
}
try {
// Read without block
int bytesAvailable = bis.available();
while (bytesAvailable > 0) {
int bytesRead = bis.read(buffer, 0, Math.min(bytesAvailable, buffer.length));
if (bytesRead > 0) {
receivedMessages.addAll(p1Parser.parseData(buffer, 0, bytesRead));
} else {
logger.debug("Expected bytes {} to read, but {} bytes were read", bytesAvailable, bytesRead);
}
bytesAvailable = bis.available();
}
} catch (IOException ioe) {
/*
* Read is interrupted. This can be due to a broken connection or
* closing the port
*/
if (portState == PortState.CLOSED) {
// Closing on purpose
logger.info("Read aborted: DSMRPort is closed");
} else {
// Closing due to broken connection
logger.warn("DSMRPort is not available anymore, closing port");
logger.debug("Caused by:", ioe);
close();
}
} catch (NullPointerException npe) {
if (portState == PortState.CLOSED) {
// Port was closed
logger.info("Read aborted: DSMRPort is closed");
} else {
logger.error("Unexpected problem occured", npe);
close();
}
}
if (portState == PortState.AUTO_DETECT && receivedMessages.size() > 0) {
portState = PortState.OPENED;
}
return receivedMessages;
}
/**
* Checks the current port state and initiate actions based on it.
* <ul>
* <li>CLOSED --> Port will be opened
* <li>AUTO_DETECT --> Auto detect period will be evaluated
* <li>OPENED --> Nothing has to be done
* </ul>
*/
private void handlePortState() {
switch (portState) {
case CLOSED:
if (open()) {
portState = PortState.AUTO_DETECT;
autoDetectTS = System.currentTimeMillis();
}
break;
case AUTO_DETECT:
if ((System.currentTimeMillis() - autoDetectTS) > autoDetectTimeoutMSec) {
logger.warn("Did not receive messages from DSMR port, switching port speed.");
switchPortSpeed();
close();
if (open()) {
portState = PortState.AUTO_DETECT;
autoDetectTS = System.currentTimeMillis();
}
}
break;
case OPENED:
/* do nothing */
break;
}
}
/**
* Switch the Serial Port speed (LOW --> HIGH and vice versa).
*/
private void switchPortSpeed() {
if (fixedPortSettings == null) {
logger.debug("No fixed port setting (autodetect ENABLED), switch between specification standard settings");
// Checking instance reference here since these are final
if (portSettings == DSMRPortSettings.HIGH_SPEED_SETTINGS) {
portSettings = DSMRPortSettings.LOW_SPEED_SETTINGS;
} else {
portSettings = DSMRPortSettings.HIGH_SPEED_SETTINGS;
}
logger.debug("Switched port settings to: {}", portSettings);
} else {
portSettings = fixedPortSettings;
logger.info("Fixed port settings configured (autodetect DISABLED): {}", portSettings);
}
}
/**
* Checks if the given port name is autodetected by gnu.io or already listed
* in the system property gnu.io.rxtx.SerialPorts
*
* @param portName String containing the port name to lookup
* @return true if port exists, false otherwise
*/
private boolean portExists(String portName) {
@SuppressWarnings("unchecked")
Enumeration<CommPortIdentifier> portEnum = CommPortIdentifier.getPortIdentifiers();
boolean portExists = false;
logger.debug("Searching autodetected ports for: {}", portName);
while (portEnum.hasMoreElements()) {
CommPortIdentifier portIdentifier = portEnum.nextElement();
if (portIdentifier.getPortType() == CommPortIdentifier.PORT_SERIAL) {
logger.debug("Found serial port: {}", portIdentifier.getName());
if (portIdentifier.getName().equals(portName)) {
portExists = true;
}
}
}
if (!portExists) {
Properties properties = System.getProperties();
String currentPorts = properties.getProperty("gnu.io.rxtx.SerialPorts", "");
if (currentPorts.indexOf(portName) >= 0) {
logger.debug("{} is listed in system property gnu.io.rxtx.SerialPorts", portName);
portExists = true;
} else {
logger.debug("{} is not listed in system property gnu.io.rxtx.SerialPorts", portName);
}
}
return portExists;
}
/**
* Opens the Operation System Serial Port
* <p>
* This method opens the port and set Serial Port parameters according to
* the DSMR specification. Since the specification is clear about these
* parameters there are not configurable.
* <p>
* If there are problem while opening the port, it is the responsibility of
* the calling method to handle this situation (and for example close the
* port again).
* <p>
* Opening an already open port is harmless. The method will return
* immediately
*
* @return true if opening was successful (or port was already open), false
* otherwise
*/
private boolean open() {
synchronized (portLock) {
// Sanity check
if (portState != PortState.CLOSED) {
return true;
}
try {
// GNU.io autodetects standard serial port names
// Add non standard port names if not exists (fixes part of #4175)
if (!portExists(portName)) {
logger.warn("Port {} does not exists according to the system, we will still try to open it",
portName);
}
// Opening Operating System Serial Port
logger.debug("Creating CommPortIdentifier");
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
logger.debug("Opening CommPortIdentifier");
CommPort commPort = portIdentifier.open("org.openhab.binding.dsmr", readTimeoutMSec);
logger.debug("Configure serial port");
serialPort = (SerialPort) commPort;
serialPort.enableReceiveThreshold(1);
serialPort.enableReceiveTimeout(readTimeoutMSec);
// Configure Serial Port based on specified port speed
logger.debug("Configure serial port parameters: {}", portSettings);
if (portSettings != null) {
serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
portSettings.getStopbits(), portSettings.getParity());
/* special settings for low speed port (checking reference here) */
if (portSettings == DSMRPortSettings.LOW_SPEED_SETTINGS) {
serialPort.setDTR(false);
serialPort.setRTS(true);
}
} else {
logger.error("Invalid port parameters, closing port:{}", portSettings);
return false;
}
} catch (NoSuchPortException nspe) {
logger.error("Could not open port: {}", portName, nspe);
return false;
} catch (PortInUseException piue) {
logger.error("Port already in use: {}", portName, piue);
return false;
} catch (UnsupportedCommOperationException ucoe) {
logger.error(
"Port does not support requested port settings " + "(invalid dsmr:portsettings parameter?): {}",
portName, ucoe);
return false;
}
// SerialPort is ready, open the reader
logger.info("SerialPort opened successful");
try {
bis = new BufferedInputStream(serialPort.getInputStream());
} catch (IOException ioe) {
logger.error("Failed to get inputstream for serialPort. Closing port", ioe);
return false;
}
logger.info("DSMR Port opened successful");
return true;
}
}
}