]> git.basschouten.com Git - openhab-addons.git/blob
efdd95c5964cbc873742e76f1c1989967c169bb2
[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;
14
15 import java.util.concurrent.ScheduledExecutorService;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.dsmr.internal.device.connector.DSMRConnectorErrorEvent;
22 import org.openhab.binding.dsmr.internal.device.connector.DSMRSerialConnector;
23 import org.openhab.binding.dsmr.internal.device.connector.DSMRSerialSettings;
24 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
25 import org.openhab.core.io.transport.serial.SerialPortManager;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 /**
30  * DSMR Serial device that auto discovers the serial port speed.
31  *
32  * @author M. Volaart - Initial contribution
33  * @author Hilbrand Bouwkamp - New class. Simplified states and contains code specific to discover the serial port
34  *         settings automatically.
35  */
36 @NonNullByDefault
37 public class DSMRSerialAutoDevice implements DSMRDevice, DSMREventListener {
38
39     /**
40      * Enum to keep track of the internal state of {@link DSMRSerialAutoDevice}.
41      */
42     enum DeviceState {
43         /**
44          * Discovers the settings of the serial port.
45          */
46         DISCOVER_SETTINGS,
47         /**
48          * Device is receiving telegram data from the serial port.
49          */
50         NORMAL,
51         /**
52          * Communication with serial port isn't working.
53          */
54         ERROR
55     }
56
57     /**
58      * When switching baudrate ignore any errors received with the given time frame. Switching baudrate causes errors
59      * and should not be interpreted as reading errors.
60      */
61     private static final long SWITCHING_BAUDRATE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
62
63     /**
64      * This factor is multiplied with the {@link #baudrateSwitchTimeoutSeconds} and used as the duration the discovery
65      * of the baudrate may take.
66      */
67     private static final long DISCOVER_TIMEOUT_FACTOR = 4L;
68
69     /*
70      * Februari 2017
71      * Due to the Dutch Smart Meter program where every residence is provided
72      * a smart for free and the smart meters are DSMR V4 or higher
73      * we assume the majority of meters communicate with HIGH_SPEED_SETTINGS
74      * For older meters this means initializing is taking probably 1 minute
75      */
76     private static final DSMRSerialSettings DEFAULT_PORT_SETTINGS = DSMRSerialSettings.HIGH_SPEED_SETTINGS;
77
78     private final Logger logger = LoggerFactory.getLogger(DSMRSerialAutoDevice.class);
79
80     /**
81      * DSMR Connector to the serial port
82      */
83     private final DSMRSerialConnector dsmrConnector;
84     private final ScheduledExecutorService scheduler;
85     private final DSMRTelegramListener telegramListener;
86
87     /**
88      * Time in seconds in which period valid data is expected during discovery. If exceeded without success the baudrate
89      * is switched
90      */
91     private final int baudrateSwitchTimeoutSeconds;
92
93     /**
94      * Serial port connection settings
95      */
96     private DSMRSerialSettings portSettings = DEFAULT_PORT_SETTINGS;
97
98     /**
99      * Keeps track of the state this instance is in.
100      */
101     private DeviceState state = DeviceState.NORMAL;
102
103     /**
104      * Timer for handling discovery of a single setting.
105      */
106     private @Nullable ScheduledFuture<?> halfTimeTimer;
107
108     /**
109      * Timer for handling end of discovery.
110      */
111     private @Nullable ScheduledFuture<?> endTimeTimer;
112
113     /**
114      * The listener of the class handling the connector events
115      */
116     private DSMREventListener parentListener;
117
118     /**
119      * Time in nanos the last time the baudrate was switched. This is used during discovery ignore errors retrieved
120      * after switching baudrate for the period set in {@link #SWITCHING_BAUDRATE_TIMEOUT_NANOS}.
121      */
122     private long lastSwitchedBaudrateNanos;
123
124     /**
125      * Creates a new {@link DSMRSerialAutoDevice}
126      *
127      * @param serialPortManager the manager to get a new serial port connecting from
128      * @param serialPortName the port name (e.g. /dev/ttyUSB0 or COM1)
129      * @param listener the parent {@link DSMREventListener}
130      * @param telegramListener listener to report found telegrams or errors
131      * @param scheduler the scheduler to use with the baudrate switching timers
132      * @param baudrateSwitchTimeoutSeconds timeout period for when to try other baudrate settings and end the discovery
133      *            of the baudrate
134      */
135     public DSMRSerialAutoDevice(SerialPortManager serialPortManager, String serialPortName, DSMREventListener listener,
136             DSMRTelegramListener telegramListener, ScheduledExecutorService scheduler,
137             int baudrateSwitchTimeoutSeconds) {
138         this.parentListener = listener;
139         this.scheduler = scheduler;
140         this.baudrateSwitchTimeoutSeconds = baudrateSwitchTimeoutSeconds;
141         this.telegramListener = telegramListener;
142         telegramListener.setDsmrEventListener(listener);
143         dsmrConnector = new DSMRSerialConnector(serialPortManager, serialPortName, telegramListener);
144         logger.debug("Initialized port '{}'", serialPortName);
145     }
146
147     @Override
148     public void start() {
149         stopDiscover(DeviceState.DISCOVER_SETTINGS);
150         portSettings = DEFAULT_PORT_SETTINGS;
151         telegramListener.setDsmrEventListener(this);
152         dsmrConnector.open(portSettings);
153         restartHalfTimer();
154         endTimeTimer = scheduler.schedule(this::endTimeScheduledCall,
155                 baudrateSwitchTimeoutSeconds * DISCOVER_TIMEOUT_FACTOR, TimeUnit.SECONDS);
156     }
157
158     @Override
159     public void restart() {
160         if (state == DeviceState.ERROR) {
161             stop();
162             start();
163         } else if (state == DeviceState.NORMAL) {
164             dsmrConnector.restart(portSettings);
165         }
166     }
167
168     @Override
169     public synchronized void stop() {
170         dsmrConnector.close();
171         stopDiscover(state);
172         logger.trace("stopped with state:{}", state);
173     }
174
175     /**
176      * Handle if telegrams are received.
177      *
178      * @param telegram the details of the received telegram
179      */
180     @Override
181     public void handleTelegramReceived(P1Telegram telegram) {
182         if (!telegram.getCosemObjects().isEmpty()) {
183             stopDiscover(DeviceState.NORMAL);
184             parentListener.handleTelegramReceived(telegram);
185             logger.info("Start receiving telegrams on port {} with settings: {}", dsmrConnector.getPortName(),
186                     portSettings);
187         }
188     }
189
190     /**
191      * Event handler for DSMR Port events.
192      *
193      * @param portEvent {@link DSMRConnectorErrorEvent} to handle
194      */
195     @Override
196     public void handleErrorEvent(DSMRConnectorErrorEvent portEvent) {
197         logger.trace("Received portEvent {}", portEvent.getEventDetails());
198         if (portEvent == DSMRConnectorErrorEvent.READ_ERROR) {
199             switchBaudrate();
200         } else {
201             logger.debug("Error during discovery of port settings: {}, current state:{}.", portEvent.getEventDetails(),
202                     state);
203             stopDiscover(DeviceState.ERROR);
204             parentListener.handleErrorEvent(portEvent);
205         }
206     }
207
208     /**
209      * @param lenientMode the lenientMode to set
210      */
211     @Override
212     public void setLenientMode(boolean lenientMode) {
213         telegramListener.setLenientMode(lenientMode);
214     }
215
216     /**
217      * @return Returns the state of the instance. Used for testing only.
218      */
219     DeviceState getState() {
220         return state;
221     }
222
223     /**
224      * Switches the baudrate on the serial port.
225      */
226     @SuppressWarnings("PMD.CompareObjectsWithEquals")
227     private void switchBaudrate() {
228         if (lastSwitchedBaudrateNanos + SWITCHING_BAUDRATE_TIMEOUT_NANOS > System.nanoTime()) {
229             // Ignore switching baudrate if this is called within the timeout after a previous switch.
230             return;
231         }
232         lastSwitchedBaudrateNanos = System.nanoTime();
233         if (state == DeviceState.DISCOVER_SETTINGS) {
234             restartHalfTimer();
235             logger.debug(
236                     "Discover port settings is running for half time now and still nothing discovered, switching baudrate and retrying");
237             portSettings = portSettings == DSMRSerialSettings.HIGH_SPEED_SETTINGS
238                     ? DSMRSerialSettings.LOW_SPEED_SETTINGS
239                     : DSMRSerialSettings.HIGH_SPEED_SETTINGS;
240             dsmrConnector.setSerialPortParams(portSettings);
241         }
242     }
243
244     /**
245      * Stops the discovery process as triggered by the end timer. It will only act if the discovery process was still
246      * running.
247      */
248     private void endTimeScheduledCall() {
249         if (state == DeviceState.DISCOVER_SETTINGS) {
250             stopDiscover(DeviceState.ERROR);
251             parentListener.handleErrorEvent(DSMRConnectorErrorEvent.DONT_EXISTS);
252         }
253     }
254
255     /**
256      * Stops the discovery of port speed process and sets the state with which it should be stopped.
257      *
258      * @param state the state with which the process was stopped.
259      */
260     private void stopDiscover(DeviceState state) {
261         telegramListener.setDsmrEventListener(parentListener);
262         logger.debug("Stop discovery of port settings.");
263         if (halfTimeTimer != null) {
264             halfTimeTimer.cancel(true);
265             halfTimeTimer = null;
266         }
267         if (endTimeTimer != null) {
268             endTimeTimer.cancel(true);
269             endTimeTimer = null;
270         }
271         this.state = state;
272     }
273
274     /**
275      * Method to (re)start the switching baudrate timer.
276      */
277     private void restartHalfTimer() {
278         if (halfTimeTimer != null) {
279             halfTimeTimer.cancel(true);
280         }
281         halfTimeTimer = scheduler.schedule(this::switchBaudrate, baudrateSwitchTimeoutSeconds, TimeUnit.SECONDS);
282     }
283 }