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.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;
31 * DSMR Serial device that auto discovers the serial port speed.
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.
38 public class DSMRSerialAutoDevice implements DSMRDevice, P1TelegramListener {
41 * Enum to keep track of the internal state of {@link DSMRSerialAutoDevice}.
45 * Discovers the settings of the serial port.
49 * Device is receiving telegram data from the serial port.
53 * Communication with serial port isn't working.
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.
62 private static final long SWITCHING_BAUDRATE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
65 * This factor is multiplied with the {@link #baudrateSwitchTimeoutSeconds} and used as the duration the discovery
66 * of the baudrate may take.
68 private static final long DISCOVER_TIMEOUT_FACTOR = 4L;
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
77 private static final DSMRSerialSettings DEFAULT_PORT_SETTINGS = DSMRSerialSettings.HIGH_SPEED_SETTINGS;
79 private final Logger logger = LoggerFactory.getLogger(DSMRSerialAutoDevice.class);
82 * DSMR Connector to the serial port
84 private final DSMRSerialConnector dsmrConnector;
85 private final ScheduledExecutorService scheduler;
86 private final DSMRTelegramListener telegramListener;
89 * Time in seconds in which period valid data is expected during discovery. If exceeded without success the baudrate
92 private final int baudrateSwitchTimeoutSeconds;
95 * Serial port connection settings
97 private DSMRSerialSettings portSettings = DEFAULT_PORT_SETTINGS;
100 * Keeps track of the state this instance is in.
102 private DeviceState state = DeviceState.NORMAL;
105 * Timer for handling discovery of a single setting.
107 private @Nullable ScheduledFuture<?> halfTimeTimer;
110 * Timer for handling end of discovery.
112 private @Nullable ScheduledFuture<?> endTimeTimer;
115 * The listener of the class handling the connector events
117 private final P1TelegramListener parentListener;
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}.
123 private long lastSwitchedBaudrateNanos;
126 * Creates a new {@link DSMRSerialAutoDevice}
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
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);
149 public void start() {
150 stopDiscover(DeviceState.DISCOVER_SETTINGS);
151 portSettings = DEFAULT_PORT_SETTINGS;
152 telegramListener.setP1TelegramListener(this);
153 dsmrConnector.open(portSettings);
155 endTimeTimer = scheduler.schedule(this::endTimeScheduledCall,
156 baudrateSwitchTimeoutSeconds * DISCOVER_TIMEOUT_FACTOR, TimeUnit.SECONDS);
160 public void restart() {
161 if (state == DeviceState.ERROR) {
164 } else if (state == DeviceState.NORMAL) {
165 dsmrConnector.restart(portSettings);
170 public synchronized void stop() {
171 dsmrConnector.close();
173 logger.trace("stopped with state:{}", state);
177 * Handle if telegrams are received.
179 * @param telegram the details of the received telegram
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(),
190 * Event handler for DSMR Port events.
192 * @param portEvent {@link DSMRErrorStatus} to handle
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) {
200 logger.debug("Error during discovery of port settings: {}, current state:{}.", portEvent.getEventDetails(),
202 stopDiscover(DeviceState.ERROR);
203 parentListener.onError(portEvent, message);
208 * @param lenientMode the lenientMode to set
211 public void setLenientMode(final boolean lenientMode) {
212 telegramListener.setLenientMode(lenientMode);
216 * @return Returns the state of the instance. Used for testing only.
218 DeviceState getState() {
223 * Switches the baudrate on the serial port.
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.
231 lastSwitchedBaudrateNanos = System.nanoTime();
232 if (state == DeviceState.DISCOVER_SETTINGS) {
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);
244 * Stops the discovery process as triggered by the end timer. It will only act if the discovery process was still
247 private void endTimeScheduledCall() {
248 if (state == DeviceState.DISCOVER_SETTINGS) {
249 stopDiscover(DeviceState.ERROR);
250 parentListener.onError(DSMRErrorStatus.PORT_DONT_EXISTS, "");
255 * Stops the discovery of port speed process and sets the state with which it should be stopped.
257 * @param state the state with which the process was stopped.
259 private void stopDiscover(final DeviceState state) {
261 telegramListener.setP1TelegramListener(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);
274 * Method to (re)start the switching baudrate timer.
276 private void restartHalfTimer() {
277 if (halfTimeTimer != null) {
278 halfTimeTimer.cancel(true);
280 halfTimeTimer = scheduler.schedule(this::switchBaudrate, baudrateSwitchTimeoutSeconds, TimeUnit.SECONDS);