2 * Copyright (c) 2010-2021 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(Bridge bridge, 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(ChannelUID channelUID, 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(DSMRDeviceConfiguration deviceConfig) {
174 final DSMRDevice dsmrDevice;
177 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
178 DSMRSerialSettings.HIGH_SPEED_SETTINGS, this, new DSMRTelegramListener(deviceConfig.decryptionKey));
180 final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
182 if (deviceConfig.isSerialFixedSettings()) {
183 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
184 DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
186 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
187 telegramListener, scheduler, deviceConfig.receivedTimeout);
194 * Registers a meter listener.
196 * @param meterListener the meter discovery listener to add
197 * @return true if listener is added, false otherwise
199 public boolean registerDSMRMeterListener(P1TelegramListener meterListener) {
200 logger.trace("Register DSMRMeterListener");
201 return meterListeners.add(meterListener);
205 * Unregisters a meter listener
207 * @param meterListener the meter discovery listener to remove
208 * @return true is listener is removed, false otherwise
210 public boolean unregisterDSMRMeterListener(P1TelegramListener meterListener) {
211 logger.trace("Unregister DSMRMeterListener");
212 return meterListeners.remove(meterListener);
216 * Watchdog method that is run with the scheduler and checks if meter values were received. If the timeout is
217 * exceeded the device is restarted. If the off line timeout factor is exceeded the device is set off line. By not
218 * setting the device on first exceed off line their is some slack in the system and it won't flip on and offline in
219 * case of an unstable system.
221 private void alive() {
222 logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
223 final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
225 if (deltaLastReceived > receivedTimeoutNanos) {
226 logger.debug("No data received for {} seconds, restarting port if possible.",
227 TimeUnit.NANOSECONDS.toSeconds(deltaLastReceived));
228 if (dsmrDeviceRunnable != null) {
229 dsmrDeviceRunnable.restart();
231 if (deltaLastReceived > receivedTimeoutNanos * OFFLINE_TIMEOUT_FACTOR) {
232 logger.trace("Setting device offline if not yet done, and reset last received time.");
233 if (getThing().getStatus() == ThingStatus.ONLINE) {
234 deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.bridge.nodata");
236 resetLastReceivedState();
242 * Sets the last received time of messages to the current time.
244 private void resetLastReceivedState() {
245 telegramReceivedTimeNanos = System.nanoTime();
246 logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
250 public synchronized void handleTelegramReceived(P1Telegram telegram) {
251 if (telegram.getCosemObjects().isEmpty()) {
252 logger.debug("Parsing worked but something went wrong, so there were no CosemObjects:{}",
253 telegram.getTelegramState().stateDetails);
254 deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, telegram.getTelegramState().stateDetails);
256 resetLastReceivedState();
257 meterValueReceived(telegram);
262 public void handleErrorEvent(DSMRConnectorErrorEvent portEvent) {
263 if (portEvent != DSMRConnectorErrorEvent.READ_ERROR) {
264 deviceOffline(ThingStatusDetail.CONFIGURATION_ERROR, portEvent.getEventDetails());
269 * Method to forward the last received messages to the bound meters and to the meterListeners.
271 * @param telegram received meter values.
273 private void meterValueReceived(P1Telegram telegram) {
274 if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
275 updateStatus(ThingStatus.ONLINE);
277 getThing().getThings().forEach(child -> {
278 if (logger.isTraceEnabled()) {
279 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
280 telegram.getCosemObjects().size());
282 final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
284 if (dsmrMeterHandler instanceof DSMRMeterHandler) {
285 dsmrMeterHandler.telegramReceived(telegram);
288 meterListeners.forEach(m -> m.telegramReceived(telegram));
292 public void dispose() {
293 if (watchdog != null) {
294 watchdog.cancel(true);
297 if (dsmrDeviceRunnable != null) {
298 dsmrDeviceRunnable.stop();
303 * @param lenientMode the lenientMode to set
305 public void setLenientMode(boolean lenientMode) {
306 logger.trace("SetLenientMode: {}", lenientMode);
307 if (dsmrDevice != null) {
308 dsmrDevice.setLenientMode(lenientMode);
313 * Convenience method to set device off line.
315 * @param status off line status
316 * @param details off line detailed message
318 private void deviceOffline(ThingStatusDetail status, String details) {
319 updateStatus(ThingStatus.OFFLINE, status, details);