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.CONFIGURATION_ADDITIONAL_KEY;
16 import static org.openhab.binding.dsmr.internal.DSMRBindingConstants.CONFIGURATION_DECRYPTION_KEY;
17 import static org.openhab.binding.dsmr.internal.DSMRBindingConstants.THING_TYPE_SMARTY_BRIDGE;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.dsmr.internal.device.DSMRDevice;
28 import org.openhab.binding.dsmr.internal.device.DSMRDeviceConfiguration;
29 import org.openhab.binding.dsmr.internal.device.DSMRDeviceRunnable;
30 import org.openhab.binding.dsmr.internal.device.DSMRFixedConfigDevice;
31 import org.openhab.binding.dsmr.internal.device.DSMRSerialAutoDevice;
32 import org.openhab.binding.dsmr.internal.device.DSMRTelegramListener;
33 import org.openhab.binding.dsmr.internal.device.connector.DSMRErrorStatus;
34 import org.openhab.binding.dsmr.internal.device.connector.DSMRSerialSettings;
35 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
36 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
37 import org.openhab.binding.dsmr.internal.discovery.DSMRMeterDiscoveryService;
38 import org.openhab.core.io.transport.serial.SerialPortManager;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.util.HexUtils;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link DSMRBridgeHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author M. Volaart - Initial contribution
55 * @author Hilbrand Bouwkamp - Refactored way messages are forwarded to meters. Removed availableMeters dependency.
58 public class DSMRBridgeHandler extends BaseBridgeHandler implements P1TelegramListener {
61 * Factor that will be multiplied with {@link #receivedTimeoutNanos} to get the timeout factor after which the
62 * device is set off line.
64 private static final int OFFLINE_TIMEOUT_FACTOR = 10;
66 private final Logger logger = LoggerFactory.getLogger(DSMRBridgeHandler.class);
69 * Additional meter listeners to get received meter values.
71 private final List<P1TelegramListener> meterListeners = new ArrayList<>();
74 * Serial Port Manager.
76 private final SerialPortManager serialPortManager;
79 * The dsmrDevice managing the connection and handling telegrams.
81 private @NonNullByDefault({}) DSMRDevice dsmrDevice;
84 * Long running process that controls the DSMR device connection.
86 private @NonNullByDefault({}) DSMRDeviceRunnable dsmrDeviceRunnable;
89 * Thread for {@link DSMRDeviceRunnable}. A thread is used because the {@link DSMRDeviceRunnable} is a blocking
90 * process that runs as long as the thing is not disposed.
92 private @NonNullByDefault({}) Thread dsmrDeviceThread;
95 * Watchdog to check if messages received and restart if necessary.
97 private @NonNullByDefault({}) ScheduledFuture<?> watchdog;
100 * Number of nanoseconds after which a timeout is triggered when no messages received.
102 private long receivedTimeoutNanos;
105 * Timestamp in nanoseconds of last P1 telegram received
107 private volatile long telegramReceivedTimeNanos;
109 private final boolean smartyMeter;
111 private @Nullable String lastKnownReadErrorMessage;
116 * @param bridge the Bridge ThingType
117 * @param serialPortManager The Serial port manager
119 public DSMRBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
121 this.serialPortManager = serialPortManager;
122 smartyMeter = THING_TYPE_SMARTY_BRIDGE.equals(bridge.getThingTypeUID());
126 public Collection<Class<? extends ThingHandlerService>> getServices() {
127 return List.of(DSMRMeterDiscoveryService.class);
131 * The {@link DSMRBridgeHandler} does not support handling commands.
133 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
134 * @param command the {@link Command}
137 public void handleCommand(final ChannelUID channelUID, final Command command) {
138 // DSMRBridgeHandler does not support commands
142 * Initializes this {@link DSMRBridgeHandler}.
144 * This method will get the corresponding configuration and initialize and start the corresponding
145 * {@link DSMRDevice}.
148 public void initialize() {
149 final DSMRDeviceConfiguration deviceConfig = getConfigAs(DSMRDeviceConfiguration.class);
151 if (smartyMeter && !validateSmartyMeterConfiguration(deviceConfig)) {
155 logger.trace("Using configuration {}", deviceConfig);
156 updateStatus(ThingStatus.UNKNOWN);
157 receivedTimeoutNanos = TimeUnit.SECONDS.toNanos(deviceConfig.receivedTimeout);
158 final DSMRDevice dsmrDevice = createDevice(deviceConfig);
159 resetLastReceivedState();
160 this.dsmrDevice = dsmrDevice; // otherwise Eclipse will give a null pointer error on the next line :-(
161 dsmrDeviceRunnable = new DSMRDeviceRunnable(dsmrDevice, this);
162 dsmrDeviceThread = new Thread(dsmrDeviceRunnable);
163 dsmrDeviceThread.setName("OH-binding-" + getThing().getUID());
164 dsmrDeviceThread.setDaemon(true);
165 dsmrDeviceThread.start();
166 watchdog = scheduler.scheduleWithFixedDelay(this::alive, receivedTimeoutNanos, receivedTimeoutNanos,
167 TimeUnit.NANOSECONDS);
170 private boolean validateSmartyMeterConfiguration(final DSMRDeviceConfiguration deviceConfig) {
172 if (deviceConfig.decryptionKey == null || deviceConfig.decryptionKey.length() != 32) {
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
174 "@text/addon.dsmr.error.configuration.invalidsmartykey");
176 } else if (!validDecryptionKey(deviceConfig.decryptionKey, CONFIGURATION_DECRYPTION_KEY)
177 || !validDecryptionKey(deviceConfig.additionalKey, CONFIGURATION_ADDITIONAL_KEY)) {
185 private boolean validDecryptionKey(final String key, final String message) {
187 HexUtils.hexToBytes(key);
189 } catch (final IllegalArgumentException e) {
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
191 "@text/addon.dsmr.error.configuration.invalid." + message + " [" + e.getMessage() + "]");
197 * Creates the {@link DSMRDevice} that corresponds with the user specified configuration.
199 * @param deviceConfig device configuration
200 * @return Specific {@link DSMRDevice} instance
202 private DSMRDevice createDevice(final DSMRDeviceConfiguration deviceConfig) {
203 final DSMRDevice dsmrDevice;
206 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
207 DSMRSerialSettings.HIGH_SPEED_SETTINGS, this,
208 new DSMRTelegramListener(deviceConfig.decryptionKey, deviceConfig.additionalKey));
210 final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
212 if (deviceConfig.isSerialFixedSettings()) {
213 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
214 DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
216 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
217 telegramListener, scheduler, deviceConfig.receivedTimeout);
224 * Registers a meter listener.
226 * @param meterListener the meter discovery listener to add
227 * @return true if listener is added, false otherwise
229 public boolean registerDSMRMeterListener(final P1TelegramListener meterListener) {
230 logger.trace("Register DSMRMeterListener");
231 return meterListeners.add(meterListener);
235 * Unregisters a meter listener
237 * @param meterListener the meter discovery listener to remove
238 * @return true is listener is removed, false otherwise
240 public boolean unregisterDSMRMeterListener(final P1TelegramListener meterListener) {
241 logger.trace("Unregister DSMRMeterListener");
242 return meterListeners.remove(meterListener);
246 * Watchdog method that is run with the scheduler and checks if meter values were received. If the timeout is
247 * exceeded the device is restarted. If the off line timeout factor is exceeded the device is set off line. By not
248 * setting the device on first exceed off line their is some slack in the system and it won't flip on and offline in
249 * case of an unstable system.
251 private void alive() {
252 logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
253 final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
255 if (deltaLastReceived > receivedTimeoutNanos) {
256 logger.debug("No valid data received for {} seconds, restarting port if possible.",
257 TimeUnit.NANOSECONDS.toSeconds(deltaLastReceived));
258 if (deltaLastReceived > receivedTimeoutNanos * OFFLINE_TIMEOUT_FACTOR) {
259 logger.trace("Setting device offline if not yet done, and reset last received time.");
260 if (isInitialized() && getThing().getStatus() != ThingStatus.OFFLINE) {
261 final String lkm = lastKnownReadErrorMessage;
262 final String message = lkm == null ? "@text/addon.dsmr.error.bridge.nodata" : lkm;
264 deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, message);
266 resetLastReceivedState();
268 if (dsmrDeviceRunnable != null) {
269 dsmrDeviceRunnable.restart();
275 * Sets the last received time of messages to the current time.
277 private void resetLastReceivedState() {
278 lastKnownReadErrorMessage = null;
279 telegramReceivedTimeNanos = System.nanoTime();
280 logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
284 public synchronized void telegramReceived(final P1Telegram telegram) {
285 resetLastReceivedState();
286 meterValueReceived(telegram);
290 public void onError(final DSMRErrorStatus errorStatus, final String message) {
291 if (errorStatus == DSMRErrorStatus.TELEGRAM_NO_DATA) {
292 logger.debug("Parsing worked but something went wrong, so there were no CosemObjects:{}", message);
293 lastKnownReadErrorMessage = errorStatus.getEventDetails();
295 final String errorMessage = errorStatus.getEventDetails() + ' ' + message;
296 lastKnownReadErrorMessage = errorMessage;
297 // if fatal set directly offline.
298 if (errorStatus.isFatal()) {
299 deviceOffline(ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
305 * Method to forward the last received messages to the bound meters and to the meterListeners.
307 * @param telegram received meter values.
309 private void meterValueReceived(final P1Telegram telegram) {
310 if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
311 updateStatus(ThingStatus.ONLINE);
313 getThing().getThings().forEach(child -> {
314 if (logger.isTraceEnabled()) {
315 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
316 telegram.getCosemObjects().size());
318 final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
320 if (dsmrMeterHandler instanceof DSMRMeterHandler) {
321 dsmrMeterHandler.telegramReceived(telegram);
324 meterListeners.forEach(m -> m.telegramReceived(telegram));
328 public void dispose() {
329 if (watchdog != null) {
330 watchdog.cancel(true);
333 if (dsmrDeviceRunnable != null) {
334 dsmrDeviceRunnable.stop();
339 * @param lenientMode the lenientMode to set
341 public void setLenientMode(final boolean lenientMode) {
342 logger.trace("SetLenientMode: {}", lenientMode);
343 if (dsmrDevice != null) {
344 dsmrDevice.setLenientMode(lenientMode);
349 * Convenience method to set device off line.
351 * @param status off line status
352 * @param details off line detailed message
354 private void deviceOffline(final ThingStatusDetail status, final String details) {
355 updateStatus(ThingStatus.OFFLINE, status, details);