]> git.basschouten.com Git - openhab-addons.git/blob
93f7124b8d742725d96532f070cafaf675610ec5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.dsmr.internal.device.connector;
14
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;
21
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;
34
35 /**
36  * Class that implements the DSMR port functionality that comply to the Dutch Smart Meter Requirements.
37  * <p>
38  * This class will handle communication with the Serial Port itself and notify listeners about events and received P1
39  * telegrams.
40  *
41  * @author M. Volaart - Initial contribution
42  * @author Hilbrand Bouwkamp - Split code and moved DSMR specific handling to separate class.
43  */
44 @NonNullByDefault
45 public class DSMRSerialConnector extends DSMRBaseConnector implements SerialPortEventListener {
46     /**
47      * Serial port read time out in milliseconds.
48      */
49     private static final int SERIAL_PORT_READ_TIMEOUT_MILLISECONDS = (int) TimeUnit.SECONDS.toMillis(15);
50
51     /**
52      * The receive threshold/time out set on the serial port.
53      */
54     private static final int SERIAL_TIMEOUT_MILLISECONDS = 1000;
55
56     private final Logger logger = LoggerFactory.getLogger(DSMRSerialConnector.class);
57
58     /**
59      * Manager to get new port from.
60      */
61     private final SerialPortManager portManager;
62
63     /**
64      * Serial port name.
65      */
66     private final String serialPortName;
67
68     /**
69      * Serial port instance.
70      */
71     private final AtomicReference<@Nullable SerialPort> serialPortReference = new AtomicReference<>();
72
73     /**
74      * DSMR Connector listener.
75      */
76     private final DSMRConnectorListener dsmrConnectorListener;
77
78     /**
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
82      */
83     private final Object portLock = new Object();
84
85     /**
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.
88      *
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
92      */
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;
99     }
100
101     public String getPortName() {
102         return serialPortName;
103     }
104
105     /**
106      * Opens the Operation System Serial Port.
107      *
108      * @param portSettings The serial port settings to open the port with
109      */
110     public void open(final DSMRSerialSettings portSettings) {
111         DSMRErrorStatus errorStatus = null;
112
113         synchronized (portLock) {
114             final SerialPortIdentifier portIdentifier = portManager.getIdentifier(serialPortName);
115             if (portIdentifier == null) {
116                 logger.debug("Port {} does not exists", serialPortName);
117
118                 errorStatus = DSMRErrorStatus.PORT_DONT_EXISTS;
119             } else {
120                 errorStatus = open(portSettings, portIdentifier);
121             }
122             if (errorStatus != null) {
123                 // handle event within lock
124                 dsmrConnectorListener.handleError(errorStatus, "");
125             }
126         }
127     }
128
129     private @Nullable DSMRErrorStatus open(final DSMRSerialSettings portSettings,
130             final SerialPortIdentifier portIdentifier) {
131         DSMRErrorStatus errorStatus = null;
132
133         try {
134             logger.trace("Opening port {}", serialPortName);
135             final SerialPort oldSerialPort = serialPortReference.get();
136
137             // Opening Operating System Serial Port
138             final SerialPort serialPort = portIdentifier.open(DSMRBindingConstants.DSMR_PORT_NAME,
139                     SERIAL_PORT_READ_TIMEOUT_MILLISECONDS);
140
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());
145
146             // SerialPort is ready, open the reader
147             logger.trace("SerialPort opened successful on {}", serialPortName);
148             open(serialPort.getInputStream());
149
150             serialPort.addEventListener(this);
151
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);
158
159             try {
160                 serialPort.enableReceiveThreshold(SERIAL_TIMEOUT_MILLISECONDS);
161             } catch (final UnsupportedCommOperationException e) {
162                 logger.debug("Enable receive threshold is unsupported");
163             }
164             try {
165                 serialPort.enableReceiveTimeout(SERIAL_TIMEOUT_MILLISECONDS);
166             } catch (final UnsupportedCommOperationException e) {
167                 logger.debug("Enable receive timeout is unsupported");
168             }
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;
174             }
175         } catch (final IOException ioe) {
176             logger.debug("Failed to get inputstream for serialPort", ioe);
177
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);
181
182             errorStatus = DSMRErrorStatus.PORT_INTERNAL_ERROR;
183         } catch (final PortInUseException piue) {
184             logger.debug("Port already in use: {}", serialPortName, piue);
185
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);
190
191             errorStatus = DSMRErrorStatus.PORT_NOT_COMPATIBLE;
192         }
193         return errorStatus;
194     }
195
196     /**
197      * Closes the serial port and release OS resources.
198      */
199     @Override
200     public void close() {
201         synchronized (portLock) {
202             logger.debug("Closing DSMR serial port");
203
204             // Stop listening for serial port events
205             SerialPort serialPort = serialPortReference.get();
206
207             if (serialPort != null) {
208                 // Let meter stop sending values
209                 serialPort.setRTS(false);
210                 serialPort.removeEventListener();
211                 try {
212                     final InputStream inputStream = serialPort.getInputStream();
213
214                     if (inputStream != null) {
215                         inputStream.close();
216                     }
217                 } catch (final IOException ioe) {
218                     logger.debug("Failed to close serial port inputstream", ioe);
219                 }
220                 serialPort.close();
221             }
222             super.close();
223             serialPort = null;
224         }
225     }
226
227     /**
228      * Set serial port settings or restarts the connection with the port settings if it's not open.
229      *
230      * @param portSettings the port settings to set on the serial port
231      */
232     public void setSerialPortParams(final DSMRSerialSettings portSettings) {
233         synchronized (portLock) {
234             if (isOpen()) {
235                 logger.debug("Update port {} with settings: {}", this.serialPortName, portSettings);
236                 try {
237                     final SerialPort serialPort = serialPortReference.get();
238
239                     if (serialPort != null) {
240                         serialPort.setSerialPortParams(portSettings.getBaudrate(), portSettings.getDataBits(),
241                                 portSettings.getStopbits(), portSettings.getParity());
242                     }
243                 } catch (final UnsupportedCommOperationException e) {
244                     logger.debug(
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(""));
249                 }
250             } else {
251                 restart(portSettings);
252             }
253         }
254     }
255
256     /**
257      * Switch the Serial Port speed (LOW --> HIGH and vice versa).
258      */
259     public void restart(final DSMRSerialSettings portSettings) {
260         synchronized (portLock) {
261             logger.trace("Restart port {} with settings: {}", this.serialPortName, portSettings);
262             close();
263             open(portSettings);
264         }
265     }
266
267     @Override
268     public void serialEvent(@Nullable final SerialPortEvent seEvent) {
269         if (seEvent == null) {
270             return;
271         }
272         if (logger.isTraceEnabled() && SerialPortEvent.DATA_AVAILABLE != seEvent.getEventType()) {
273             logger.trace("Serial event: {}, new value:{}", seEvent.getEventType(), seEvent.getNewValue());
274         }
275         try {
276             switch (seEvent.getEventType()) {
277                 case SerialPortEvent.DATA_AVAILABLE:
278                     handleDataAvailable();
279                     break;
280                 case SerialPortEvent.BI:
281                     handleErrorEvent("Break interrupt", seEvent);
282                     break;
283                 case SerialPortEvent.FE:
284                     handleErrorEvent("Frame error", seEvent);
285                     break;
286                 case SerialPortEvent.OE:
287                     handleErrorEvent("Overrun error", seEvent);
288                     break;
289                 case SerialPortEvent.PE:
290                     handleErrorEvent("Parity error", seEvent);
291                     break;
292                 default: // do nothing
293             }
294         } catch (final RuntimeException e) {
295             logger.warn("RuntimeException during handling serial event: {}", seEvent.getEventType(), e);
296         }
297     }
298
299     /**
300      * Handles an error event. If open and it's a new value it should be handled by the listener.
301      *
302      * @param typeName type of the event, used in logging only
303      * @param portEvent Serial port event that triggered the error.
304      */
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, "");
309         }
310     }
311
312     @Override
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");
317             return;
318         }
319         super.handleDataAvailable();
320     }
321 }