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