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;
15 import java.util.concurrent.ScheduledExecutorService;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
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;
30 * DSMR Serial device that auto discovers the serial port speed.
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.
37 public class DSMRSerialAutoDevice implements DSMRDevice, DSMREventListener {
40 * Enum to keep track of the internal state of {@link DSMRSerialAutoDevice}.
44 * Discovers the settings of the serial port.
48 * Device is receiving telegram data from the serial port.
52 * Communication with serial port isn't working.
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.
61 private static final long SWITCHING_BAUDRATE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
64 * This factor is multiplied with the {@link #baudrateSwitchTimeoutSeconds} and used as the duration the discovery
65 * of the baudrate may take.
67 private static final long DISCOVER_TIMEOUT_FACTOR = 4L;
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
76 private static final DSMRSerialSettings DEFAULT_PORT_SETTINGS = DSMRSerialSettings.HIGH_SPEED_SETTINGS;
78 private final Logger logger = LoggerFactory.getLogger(DSMRSerialAutoDevice.class);
81 * DSMR Connector to the serial port
83 private final DSMRSerialConnector dsmrConnector;
84 private final ScheduledExecutorService scheduler;
85 private final DSMRTelegramListener telegramListener;
88 * Time in seconds in which period valid data is expected during discovery. If exceeded without success the baudrate
91 private final int baudrateSwitchTimeoutSeconds;
94 * Serial port connection settings
96 private DSMRSerialSettings portSettings = DEFAULT_PORT_SETTINGS;
99 * Keeps track of the state this instance is in.
101 private DeviceState state = DeviceState.NORMAL;
104 * Timer for handling discovery of a single setting.
106 private @Nullable ScheduledFuture<?> halfTimeTimer;
109 * Timer for handling end of discovery.
111 private @Nullable ScheduledFuture<?> endTimeTimer;
114 * The listener of the class handling the connector events
116 private DSMREventListener parentListener;
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}.
122 private long lastSwitchedBaudrateNanos;
125 * Creates a new {@link DSMRSerialAutoDevice}
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
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);
148 public void start() {
149 stopDiscover(DeviceState.DISCOVER_SETTINGS);
150 portSettings = DEFAULT_PORT_SETTINGS;
151 telegramListener.setDsmrEventListener(this);
152 dsmrConnector.open(portSettings);
154 endTimeTimer = scheduler.schedule(this::endTimeScheduledCall,
155 baudrateSwitchTimeoutSeconds * DISCOVER_TIMEOUT_FACTOR, TimeUnit.SECONDS);
159 public void restart() {
160 if (state == DeviceState.ERROR) {
163 } else if (state == DeviceState.NORMAL) {
164 dsmrConnector.restart(portSettings);
169 public synchronized void stop() {
170 dsmrConnector.close();
172 logger.trace("stopped with state:{}", state);
176 * Handle if telegrams are received.
178 * @param telegram the details of the received telegram
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(),
191 * Event handler for DSMR Port events.
193 * @param portEvent {@link DSMRConnectorErrorEvent} to handle
196 public void handleErrorEvent(DSMRConnectorErrorEvent portEvent) {
197 logger.trace("Received portEvent {}", portEvent.getEventDetails());
198 if (portEvent == DSMRConnectorErrorEvent.READ_ERROR) {
201 logger.debug("Error during discovery of port settings: {}, current state:{}.", portEvent.getEventDetails(),
203 stopDiscover(DeviceState.ERROR);
204 parentListener.handleErrorEvent(portEvent);
209 * @param lenientMode the lenientMode to set
212 public void setLenientMode(boolean lenientMode) {
213 telegramListener.setLenientMode(lenientMode);
217 * @return Returns the state of the instance. Used for testing only.
219 DeviceState getState() {
224 * Switches the baudrate on the serial port.
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.
232 lastSwitchedBaudrateNanos = System.nanoTime();
233 if (state == DeviceState.DISCOVER_SETTINGS) {
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);
245 * Stops the discovery process as triggered by the end timer. It will only act if the discovery process was still
248 private void endTimeScheduledCall() {
249 if (state == DeviceState.DISCOVER_SETTINGS) {
250 stopDiscover(DeviceState.ERROR);
251 parentListener.handleErrorEvent(DSMRConnectorErrorEvent.DONT_EXISTS);
256 * Stops the discovery of port speed process and sets the state with which it should be stopped.
258 * @param state the state with which the process was stopped.
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;
267 if (endTimeTimer != null) {
268 endTimeTimer.cancel(true);
275 * Method to (re)start the switching baudrate timer.
277 private void restartHalfTimer() {
278 if (halfTimeTimer != null) {
279 halfTimeTimer.cancel(true);
281 halfTimeTimer = scheduler.schedule(this::switchBaudrate, baudrateSwitchTimeoutSeconds, TimeUnit.SECONDS);