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