2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dsmr.internal.device.connector;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.TooManyListenersException;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.atomic.AtomicReference;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
24 import org.openhab.core.io.transport.serial.PortInUseException;
25 import org.openhab.core.io.transport.serial.SerialPort;
26 import org.openhab.core.io.transport.serial.SerialPortEvent;
27 import org.openhab.core.io.transport.serial.SerialPortEventListener;
28 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
29 import org.openhab.core.io.transport.serial.SerialPortManager;
30 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * Class that implements the DSMR port functionality that comply to the Dutch Smart Meter Requirements.
37 * This class will handle communication with the Serial Port itself and notify listeners about events and received P1
40 * @author M. Volaart - Initial contribution
41 * @author Hilbrand Bouwkamp - Split code and moved DSMR specific handling to separate class.
44 public class DSMRSerialConnector extends DSMRBaseConnector implements SerialPortEventListener {
46 * Serial port read time out in milliseconds.
48 private static final int SERIAL_PORT_READ_TIMEOUT_MILLISECONDS = (int) TimeUnit.SECONDS.toMillis(15);
51 * The receive threshold/time out set on the serial port.
53 private static final int SERIAL_TIMEOUT_MILLISECONDS = 1000;
55 private final Logger logger = LoggerFactory.getLogger(DSMRSerialConnector.class);
58 * Manager to get new port from.
60 private final SerialPortManager portManager;
65 private final String serialPortName;
68 * Serial port instance.
70 private AtomicReference<@Nullable SerialPort> serialPortReference = new AtomicReference<>();
73 * DSMR Connector listener.
75 private final DSMRConnectorListener dsmrConnectorListener;
78 * The portLock is used for the shared data used when opening and closing
79 * the port. The following shared data must be guarded by the lock:
80 * SerialPort, BufferedReader
82 private final Object portLock = new Object();
85 * Creates a new DSMR serial connector. This is only a reference to a port. The port will
86 * not be opened nor it is checked if the DSMR Port can successfully be opened.
88 * @param portManager Serial Port Manager
89 * @param serialPortName Device identifier of the port (e.g. /dev/ttyUSB0)
90 * @param dsmrConnectorListener The listener to send error or received data from the port
92 public DSMRSerialConnector(SerialPortManager portManager, String serialPortName,
93 DSMRConnectorListener dsmrConnectorListener) {
94 super(dsmrConnectorListener);
95 this.portManager = portManager;
96 this.serialPortName = serialPortName;
97 this.dsmrConnectorListener = dsmrConnectorListener;
100 public String getPortName() {
101 return serialPortName;
105 * Opens the Operation System Serial Port.
107 * @param portSettings The serial port settings to open the port with
109 public void open(DSMRSerialSettings portSettings) {
110 DSMRConnectorErrorEvent errorEvent = null;
112 synchronized (portLock) {
113 SerialPortIdentifier portIdentifier = portManager.getIdentifier(serialPortName);
114 if (portIdentifier == null) {
115 logger.debug("Port {} does not exists", serialPortName);
117 errorEvent = DSMRConnectorErrorEvent.DONT_EXISTS;
119 errorEvent = open(portSettings, portIdentifier);
121 if (errorEvent != null) {
122 // handle event within lock
123 dsmrConnectorListener.handleErrorEvent(errorEvent);
128 private @Nullable DSMRConnectorErrorEvent open(DSMRSerialSettings portSettings,
129 SerialPortIdentifier portIdentifier) {
130 DSMRConnectorErrorEvent errorEvent = null;
133 logger.trace("Opening port {}", serialPortName);
134 SerialPort oldSerialPort = serialPortReference.get();
136 // Opening Operating System Serial Port
137 SerialPort serialPort = portIdentifier.open(DSMRBindingConstants.DSMR_PORT_NAME,
138 SERIAL_PORT_READ_TIMEOUT_MILLISECONDS);
140 // Configure Serial Port based on specified port speed
141 logger.trace("Configure serial port parameters: {}", portSettings);
142 serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
143 portSettings.getStopbits(), portSettings.getParity());
145 // SerialPort is ready, open the reader
146 logger.trace("SerialPort opened successful on {}", serialPortName);
147 open(serialPort.getInputStream());
149 serialPort.addEventListener(this);
151 // Start listening for events
152 serialPort.notifyOnDataAvailable(true);
153 serialPort.notifyOnBreakInterrupt(true);
154 serialPort.notifyOnFramingError(true);
155 serialPort.notifyOnOverrunError(true);
156 serialPort.notifyOnParityError(true);
159 serialPort.enableReceiveThreshold(SERIAL_TIMEOUT_MILLISECONDS);
160 } catch (UnsupportedCommOperationException e) {
161 logger.debug("Enable receive threshold is unsupported");
164 serialPort.enableReceiveTimeout(SERIAL_TIMEOUT_MILLISECONDS);
165 } catch (UnsupportedCommOperationException e) {
166 logger.debug("Enable receive timeout is unsupported");
168 // The binding is ready, let the meter know we want to receive values
169 serialPort.setRTS(true);
170 if (!serialPortReference.compareAndSet(oldSerialPort, serialPort)) {
171 logger.warn("Possible bug because a new serial port value was set during opening new port.");
172 errorEvent = DSMRConnectorErrorEvent.INTERNAL_ERROR;
174 } catch (IOException ioe) {
175 logger.debug("Failed to get inputstream for serialPort", ioe);
177 errorEvent = DSMRConnectorErrorEvent.READ_ERROR;
178 } catch (TooManyListenersException tmle) {
179 logger.warn("Possible bug because a listener was added while one already set.", tmle);
181 errorEvent = DSMRConnectorErrorEvent.INTERNAL_ERROR;
182 } catch (PortInUseException piue) {
183 logger.debug("Port already in use: {}", serialPortName, piue);
185 errorEvent = DSMRConnectorErrorEvent.IN_USE;
186 } catch (UnsupportedCommOperationException ucoe) {
187 logger.debug("Port does not support requested port settings (invalid dsmr:portsettings parameter?): {}",
188 serialPortName, ucoe);
190 errorEvent = DSMRConnectorErrorEvent.NOT_COMPATIBLE;
196 * Closes the serial port and release OS resources.
199 public void close() {
200 synchronized (portLock) {
201 logger.debug("Closing DSMR serial port");
203 // Stop listening for serial port events
204 SerialPort serialPort = serialPortReference.get();
206 if (serialPort != null) {
207 // Let meter stop sending values
208 serialPort.setRTS(false);
209 serialPort.removeEventListener();
211 InputStream inputStream = serialPort.getInputStream();
213 if (inputStream != null) {
216 } catch (IOException ioe) {
217 logger.debug("Failed to close serial port inputstream", ioe);
227 * Set serial port settings or restarts the connection with the port settings if it's not open.
229 * @param portSettings the port settings to set on the serial port
231 public void setSerialPortParams(DSMRSerialSettings portSettings) {
232 synchronized (portLock) {
234 logger.debug("Update port {} with settings: {}", this.serialPortName, portSettings);
236 SerialPort serialPort = serialPortReference.get();
238 if (serialPort != null) {
239 serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
240 portSettings.getStopbits(), portSettings.getParity());
242 } catch (UnsupportedCommOperationException e) {
244 "Port does {} not support requested port settings (invalid dsmr:portsettings parameter?): {}",
245 serialPortName, portSettings);
246 dsmrConnectorListener.handleErrorEvent(DSMRConnectorErrorEvent.NOT_COMPATIBLE);
249 restart(portSettings);
255 * Switch the Serial Port speed (LOW --> HIGH and vice versa).
257 public void restart(DSMRSerialSettings portSettings) {
258 synchronized (portLock) {
259 logger.trace("Restart port {} with settings: {}", this.serialPortName, portSettings);
266 public void serialEvent(@Nullable SerialPortEvent seEvent) {
267 if (seEvent == null) {
270 if (logger.isTraceEnabled() && SerialPortEvent.DATA_AVAILABLE != seEvent.getEventType()) {
271 logger.trace("Serial event: {}, new value:{}", seEvent.getEventType(), seEvent.getNewValue());
274 switch (seEvent.getEventType()) {
275 case SerialPortEvent.DATA_AVAILABLE:
276 handleDataAvailable();
278 case SerialPortEvent.BI:
279 handleErrorEvent("Break interrupt", seEvent);
281 case SerialPortEvent.FE:
282 handleErrorEvent("Frame error", seEvent);
284 case SerialPortEvent.OE:
285 handleErrorEvent("Overrun error", seEvent);
287 case SerialPortEvent.PE:
288 handleErrorEvent("Parity error", seEvent);
290 default: // do nothing
292 } catch (RuntimeException e) {
293 logger.warn("RuntimeException during handling serial event: {}", seEvent.getEventType(), e);
298 * Handles an error event. If open and it's a new value it should be handled by the listener.
300 * @param typeName type of the event, used in logging only
301 * @param portEvent Serial port event that triggered the error.
303 private void handleErrorEvent(String typeName, SerialPortEvent portEvent) {
304 if (isOpen() && portEvent.getNewValue()) {
305 logger.trace("New DSMR port {} event", typeName);
306 dsmrConnectorListener.handleErrorEvent(DSMRConnectorErrorEvent.READ_ERROR);
311 protected void handleDataAvailable() {
312 // open port if it is not open
313 if (serialPortReference.get() == null) {
314 logger.debug("Serial port is not open, no values will be read");
317 super.handleDataAvailable();