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.Optional;
18 import java.util.TooManyListenersException;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.atomic.AtomicReference;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
25 import org.openhab.core.io.transport.serial.PortInUseException;
26 import org.openhab.core.io.transport.serial.SerialPort;
27 import org.openhab.core.io.transport.serial.SerialPortEvent;
28 import org.openhab.core.io.transport.serial.SerialPortEventListener;
29 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
30 import org.openhab.core.io.transport.serial.SerialPortManager;
31 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * Class that implements the DSMR port functionality that comply to the Dutch Smart Meter Requirements.
38 * This class will handle communication with the Serial Port itself and notify listeners about events and received P1
41 * @author M. Volaart - Initial contribution
42 * @author Hilbrand Bouwkamp - Split code and moved DSMR specific handling to separate class.
45 public class DSMRSerialConnector extends DSMRBaseConnector implements SerialPortEventListener {
47 * Serial port read time out in milliseconds.
49 private static final int SERIAL_PORT_READ_TIMEOUT_MILLISECONDS = (int) TimeUnit.SECONDS.toMillis(15);
52 * The receive threshold/time out set on the serial port.
54 private static final int SERIAL_TIMEOUT_MILLISECONDS = 1000;
56 private final Logger logger = LoggerFactory.getLogger(DSMRSerialConnector.class);
59 * Manager to get new port from.
61 private final SerialPortManager portManager;
66 private final String serialPortName;
69 * Serial port instance.
71 private final AtomicReference<@Nullable SerialPort> serialPortReference = new AtomicReference<>();
74 * DSMR Connector listener.
76 private final DSMRConnectorListener dsmrConnectorListener;
79 * The portLock is used for the shared data used when opening and closing
80 * the port. The following shared data must be guarded by the lock:
81 * SerialPort, BufferedReader
83 private final Object portLock = new Object();
86 * Creates a new DSMR serial connector. This is only a reference to a port. The port will
87 * not be opened nor it is checked if the DSMR Port can successfully be opened.
89 * @param portManager Serial Port Manager
90 * @param serialPortName Device identifier of the port (e.g. /dev/ttyUSB0)
91 * @param dsmrConnectorListener The listener to send error or received data from the port
93 public DSMRSerialConnector(final SerialPortManager portManager, final String serialPortName,
94 final DSMRConnectorListener dsmrConnectorListener) {
95 super(dsmrConnectorListener);
96 this.portManager = portManager;
97 this.serialPortName = serialPortName;
98 this.dsmrConnectorListener = dsmrConnectorListener;
101 public String getPortName() {
102 return serialPortName;
106 * Opens the Operation System Serial Port.
108 * @param portSettings The serial port settings to open the port with
110 public void open(final DSMRSerialSettings portSettings) {
111 DSMRErrorStatus errorStatus = null;
113 synchronized (portLock) {
114 final SerialPortIdentifier portIdentifier = portManager.getIdentifier(serialPortName);
115 if (portIdentifier == null) {
116 logger.debug("Port {} does not exists", serialPortName);
118 errorStatus = DSMRErrorStatus.PORT_DONT_EXISTS;
120 errorStatus = open(portSettings, portIdentifier);
122 if (errorStatus != null) {
123 // handle event within lock
124 dsmrConnectorListener.handleError(errorStatus, "");
129 private @Nullable DSMRErrorStatus open(final DSMRSerialSettings portSettings,
130 final SerialPortIdentifier portIdentifier) {
131 DSMRErrorStatus errorStatus = null;
134 logger.trace("Opening port {}", serialPortName);
135 final SerialPort oldSerialPort = serialPortReference.get();
137 // Opening Operating System Serial Port
138 final SerialPort serialPort = portIdentifier.open(DSMRBindingConstants.DSMR_PORT_NAME,
139 SERIAL_PORT_READ_TIMEOUT_MILLISECONDS);
141 // Configure Serial Port based on specified port speed
142 logger.trace("Configure serial port parameters: {}", portSettings);
143 serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
144 portSettings.getStopbits(), portSettings.getParity());
146 // SerialPort is ready, open the reader
147 logger.trace("SerialPort opened successful on {}", serialPortName);
148 open(serialPort.getInputStream());
150 serialPort.addEventListener(this);
152 // Start listening for events
153 serialPort.notifyOnDataAvailable(true);
154 serialPort.notifyOnBreakInterrupt(true);
155 serialPort.notifyOnFramingError(true);
156 serialPort.notifyOnOverrunError(true);
157 serialPort.notifyOnParityError(true);
160 serialPort.enableReceiveThreshold(SERIAL_TIMEOUT_MILLISECONDS);
161 } catch (final UnsupportedCommOperationException e) {
162 logger.debug("Enable receive threshold is unsupported");
165 serialPort.enableReceiveTimeout(SERIAL_TIMEOUT_MILLISECONDS);
166 } catch (final UnsupportedCommOperationException e) {
167 logger.debug("Enable receive timeout is unsupported");
169 // The binding is ready, let the meter know we want to receive values
170 serialPort.setRTS(true);
171 if (!serialPortReference.compareAndSet(oldSerialPort, serialPort)) {
172 logger.warn("Possible bug because a new serial port value was set during opening new port.");
173 errorStatus = DSMRErrorStatus.PORT_INTERNAL_ERROR;
175 } catch (final IOException ioe) {
176 logger.debug("Failed to get inputstream for serialPort", ioe);
178 errorStatus = DSMRErrorStatus.SERIAL_DATA_READ_ERROR;
179 } catch (final TooManyListenersException tmle) {
180 logger.warn("Possible bug because a listener was added while one already set.", tmle);
182 errorStatus = DSMRErrorStatus.PORT_INTERNAL_ERROR;
183 } catch (final PortInUseException piue) {
184 logger.debug("Port already in use: {}", serialPortName, piue);
186 errorStatus = DSMRErrorStatus.PORT_IN_USE;
187 } catch (final UnsupportedCommOperationException ucoe) {
188 logger.debug("Port does not support requested port settings (invalid dsmr:portsettings parameter?): {}",
189 serialPortName, ucoe);
191 errorStatus = DSMRErrorStatus.PORT_NOT_COMPATIBLE;
197 * Closes the serial port and release OS resources.
200 public void close() {
201 synchronized (portLock) {
202 logger.debug("Closing DSMR serial port");
204 // Stop listening for serial port events
205 SerialPort serialPort = serialPortReference.get();
207 if (serialPort != null) {
208 // Let meter stop sending values
209 serialPort.setRTS(false);
210 serialPort.removeEventListener();
212 final InputStream inputStream = serialPort.getInputStream();
214 if (inputStream != null) {
217 } catch (final IOException ioe) {
218 logger.debug("Failed to close serial port inputstream", ioe);
228 * Set serial port settings or restarts the connection with the port settings if it's not open.
230 * @param portSettings the port settings to set on the serial port
232 public void setSerialPortParams(final DSMRSerialSettings portSettings) {
233 synchronized (portLock) {
235 logger.debug("Update port {} with settings: {}", this.serialPortName, portSettings);
237 final SerialPort serialPort = serialPortReference.get();
239 if (serialPort != null) {
240 serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
241 portSettings.getStopbits(), portSettings.getParity());
243 } catch (final UnsupportedCommOperationException e) {
245 "Port does {} not support requested port settings (invalid dsmr:portsettings parameter?): {}",
246 serialPortName, portSettings);
247 dsmrConnectorListener.handleError(DSMRErrorStatus.PORT_NOT_COMPATIBLE,
248 Optional.ofNullable(e.getMessage()).orElse(""));
251 restart(portSettings);
257 * Switch the Serial Port speed (LOW --> HIGH and vice versa).
259 public void restart(final DSMRSerialSettings portSettings) {
260 synchronized (portLock) {
261 logger.trace("Restart port {} with settings: {}", this.serialPortName, portSettings);
268 public void serialEvent(@Nullable final SerialPortEvent seEvent) {
269 if (seEvent == null) {
272 if (logger.isTraceEnabled() && SerialPortEvent.DATA_AVAILABLE != seEvent.getEventType()) {
273 logger.trace("Serial event: {}, new value:{}", seEvent.getEventType(), seEvent.getNewValue());
276 switch (seEvent.getEventType()) {
277 case SerialPortEvent.DATA_AVAILABLE:
278 handleDataAvailable();
280 case SerialPortEvent.BI:
281 handleErrorEvent("Break interrupt", seEvent);
283 case SerialPortEvent.FE:
284 handleErrorEvent("Frame error", seEvent);
286 case SerialPortEvent.OE:
287 handleErrorEvent("Overrun error", seEvent);
289 case SerialPortEvent.PE:
290 handleErrorEvent("Parity error", seEvent);
292 default: // do nothing
294 } catch (final RuntimeException e) {
295 logger.warn("RuntimeException during handling serial event: {}", seEvent.getEventType(), e);
300 * Handles an error event. If open and it's a new value it should be handled by the listener.
302 * @param typeName type of the event, used in logging only
303 * @param portEvent Serial port event that triggered the error.
305 private void handleErrorEvent(final String typeName, final SerialPortEvent portEvent) {
306 if (isOpen() && portEvent.getNewValue()) {
307 logger.trace("New DSMR port {} event", typeName);
308 dsmrConnectorListener.handleError(DSMRErrorStatus.SERIAL_DATA_READ_ERROR, "");
313 protected void handleDataAvailable() {
314 // open port if it is not open
315 if (serialPortReference.get() == null) {
316 logger.debug("Serial port is not open, no values will be read");
319 super.handleDataAvailable();