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.handler;
15 import static org.openhab.binding.dsmr.internal.DSMRBindingConstants.THING_TYPE_SMARTY_BRIDGE;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.dsmr.internal.device.DSMRDevice;
25 import org.openhab.binding.dsmr.internal.device.DSMRDeviceConfiguration;
26 import org.openhab.binding.dsmr.internal.device.DSMRDeviceRunnable;
27 import org.openhab.binding.dsmr.internal.device.DSMREventListener;
28 import org.openhab.binding.dsmr.internal.device.DSMRFixedConfigDevice;
29 import org.openhab.binding.dsmr.internal.device.DSMRSerialAutoDevice;
30 import org.openhab.binding.dsmr.internal.device.DSMRTelegramListener;
31 import org.openhab.binding.dsmr.internal.device.connector.DSMRConnectorErrorEvent;
32 import org.openhab.binding.dsmr.internal.device.connector.DSMRSerialSettings;
33 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
34 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
35 import org.openhab.binding.dsmr.internal.discovery.DSMRMeterDiscoveryService;
36 import org.openhab.core.io.transport.serial.SerialPortManager;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.thing.binding.ThingHandlerService;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link DSMRBridgeHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author M. Volaart - Initial contribution
52 * @author Hilbrand Bouwkamp - Refactored way messages are forwarded to meters. Removed availableMeters dependency.
55 public class DSMRBridgeHandler extends BaseBridgeHandler implements DSMREventListener {
58 * Factor that will be multiplied with {@link #receivedTimeoutNanos} to get the timeout factor after which the
59 * device is set off line.
61 private static final int OFFLINE_TIMEOUT_FACTOR = 10;
63 private final Logger logger = LoggerFactory.getLogger(DSMRBridgeHandler.class);
66 * Additional meter listeners to get received meter values.
68 private final List<P1TelegramListener> meterListeners = new ArrayList<>();
71 * Serial Port Manager.
73 private final SerialPortManager serialPortManager;
76 * The dsmrDevice managing the connection and handling telegrams.
78 private @NonNullByDefault({}) DSMRDevice dsmrDevice;
81 * Long running process that controls the DSMR device connection.
83 private @NonNullByDefault({}) DSMRDeviceRunnable dsmrDeviceRunnable;
86 * Thread for {@link DSMRDeviceRunnable}. A thread is used because the {@link DSMRDeviceRunnable} is a blocking
87 * process that runs as long as the thing is not disposed.
89 private @NonNullByDefault({}) Thread dsmrDeviceThread;
92 * Watchdog to check if messages received and restart if necessary.
94 private @NonNullByDefault({}) ScheduledFuture<?> watchdog;
97 * Number of nanoseconds after which a timeout is triggered when no messages received.
99 private long receivedTimeoutNanos;
102 * Timestamp in nanoseconds of last P1 telegram received
104 private volatile long telegramReceivedTimeNanos;
106 private final boolean smartyMeter;
111 * @param bridge the Bridge ThingType
112 * @param serialPortManager The Serial port manager
114 public DSMRBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
116 this.serialPortManager = serialPortManager;
117 smartyMeter = THING_TYPE_SMARTY_BRIDGE.equals(bridge.getThingTypeUID());
121 public Collection<Class<? extends ThingHandlerService>> getServices() {
122 return List.of(DSMRMeterDiscoveryService.class);
126 * The {@link DSMRBridgeHandler} does not support handling commands.
128 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
129 * @param command the {@link Command}
132 public void handleCommand(final ChannelUID channelUID, final Command command) {
133 // DSMRBridgeHandler does not support commands
137 * Initializes this {@link DSMRBridgeHandler}.
139 * This method will get the corresponding configuration and initialize and start the corresponding
140 * {@link DSMRDevice}.
143 public void initialize() {
144 final DSMRDeviceConfiguration deviceConfig = getConfigAs(DSMRDeviceConfiguration.class);
146 if (smartyMeter && (deviceConfig.decryptionKey == null || deviceConfig.decryptionKey.length() != 32)) {
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148 "@text/error.configuration.invalidsmartykey");
152 logger.trace("Using configuration {}", deviceConfig);
153 updateStatus(ThingStatus.UNKNOWN);
154 receivedTimeoutNanos = TimeUnit.SECONDS.toNanos(deviceConfig.receivedTimeout);
155 final DSMRDevice dsmrDevice = createDevice(deviceConfig);
156 resetLastReceivedState();
157 this.dsmrDevice = dsmrDevice; // otherwise Eclipse will give a null pointer error on the next line :-(
158 dsmrDeviceRunnable = new DSMRDeviceRunnable(dsmrDevice, this);
159 dsmrDeviceThread = new Thread(dsmrDeviceRunnable);
160 dsmrDeviceThread.setName("OH-binding-" + getThing().getUID());
161 dsmrDeviceThread.setDaemon(true);
162 dsmrDeviceThread.start();
163 watchdog = scheduler.scheduleWithFixedDelay(this::alive, receivedTimeoutNanos, receivedTimeoutNanos,
164 TimeUnit.NANOSECONDS);
168 * Creates the {@link DSMRDevice} that corresponds with the user specified configuration.
170 * @param deviceConfig device configuration
171 * @return Specific {@link DSMRDevice} instance
173 private DSMRDevice createDevice(final DSMRDeviceConfiguration deviceConfig) {
174 final DSMRDevice dsmrDevice;
177 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
178 DSMRSerialSettings.HIGH_SPEED_SETTINGS, this,
179 new DSMRTelegramListener(deviceConfig.decryptionKey, deviceConfig.additionalKey));
181 final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
183 if (deviceConfig.isSerialFixedSettings()) {
184 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
185 DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
187 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
188 telegramListener, scheduler, deviceConfig.receivedTimeout);
195 * Registers a meter listener.
197 * @param meterListener the meter discovery listener to add
198 * @return true if listener is added, false otherwise
200 public boolean registerDSMRMeterListener(final P1TelegramListener meterListener) {
201 logger.trace("Register DSMRMeterListener");
202 return meterListeners.add(meterListener);
206 * Unregisters a meter listener
208 * @param meterListener the meter discovery listener to remove
209 * @return true is listener is removed, false otherwise
211 public boolean unregisterDSMRMeterListener(final P1TelegramListener meterListener) {
212 logger.trace("Unregister DSMRMeterListener");
213 return meterListeners.remove(meterListener);
217 * Watchdog method that is run with the scheduler and checks if meter values were received. If the timeout is
218 * exceeded the device is restarted. If the off line timeout factor is exceeded the device is set off line. By not
219 * setting the device on first exceed off line their is some slack in the system and it won't flip on and offline in
220 * case of an unstable system.
222 private void alive() {
223 logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
224 final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
226 if (deltaLastReceived > receivedTimeoutNanos) {
227 logger.debug("No data received for {} seconds, restarting port if possible.",
228 TimeUnit.NANOSECONDS.toSeconds(deltaLastReceived));
229 if (dsmrDeviceRunnable != null) {
230 dsmrDeviceRunnable.restart();
232 if (deltaLastReceived > receivedTimeoutNanos * OFFLINE_TIMEOUT_FACTOR) {
233 logger.trace("Setting device offline if not yet done, and reset last received time.");
234 if (getThing().getStatus() == ThingStatus.ONLINE) {
235 deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.bridge.nodata");
237 resetLastReceivedState();
243 * Sets the last received time of messages to the current time.
245 private void resetLastReceivedState() {
246 telegramReceivedTimeNanos = System.nanoTime();
247 logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
251 public synchronized void handleTelegramReceived(final P1Telegram telegram) {
252 if (telegram.getCosemObjects().isEmpty()) {
253 logger.debug("Parsing worked but something went wrong, so there were no CosemObjects:{}",
254 telegram.getTelegramState().stateDetails);
255 deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, telegram.getTelegramState().stateDetails);
257 resetLastReceivedState();
258 meterValueReceived(telegram);
263 public void handleErrorEvent(final DSMRConnectorErrorEvent portEvent) {
264 if (portEvent != DSMRConnectorErrorEvent.READ_ERROR) {
265 deviceOffline(ThingStatusDetail.CONFIGURATION_ERROR, portEvent.getEventDetails());
270 * Method to forward the last received messages to the bound meters and to the meterListeners.
272 * @param telegram received meter values.
274 private void meterValueReceived(final P1Telegram telegram) {
275 if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
276 updateStatus(ThingStatus.ONLINE);
278 getThing().getThings().forEach(child -> {
279 if (logger.isTraceEnabled()) {
280 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
281 telegram.getCosemObjects().size());
283 final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
285 if (dsmrMeterHandler instanceof DSMRMeterHandler) {
286 dsmrMeterHandler.telegramReceived(telegram);
289 meterListeners.forEach(m -> m.telegramReceived(telegram));
293 public void dispose() {
294 if (watchdog != null) {
295 watchdog.cancel(true);
298 if (dsmrDeviceRunnable != null) {
299 dsmrDeviceRunnable.stop();
304 * @param lenientMode the lenientMode to set
306 public void setLenientMode(final boolean lenientMode) {
307 logger.trace("SetLenientMode: {}", lenientMode);
308 if (dsmrDevice != null) {
309 dsmrDevice.setLenientMode(lenientMode);
314 * Convenience method to set device off line.
316 * @param status off line status
317 * @param details off line detailed message
319 private void deviceOffline(final ThingStatusDetail status, final String details) {
320 updateStatus(ThingStatus.OFFLINE, status, details);