]> git.basschouten.com Git - openhab-addons.git/blob
e4ce9c3562553a763c0bf8913186eee3cfe2a2ea
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.dsmr.internal.handler;
14
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;
18
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;
24
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;
49
50 /**
51  * The {@link DSMRBridgeHandler} is responsible for handling commands, which are
52  * sent to one of the channels.
53  *
54  * @author M. Volaart - Initial contribution
55  * @author Hilbrand Bouwkamp - Refactored way messages are forwarded to meters. Removed availableMeters dependency.
56  */
57 @NonNullByDefault
58 public class DSMRBridgeHandler extends BaseBridgeHandler implements P1TelegramListener {
59
60     /**
61      * Factor that will be multiplied with {@link #receivedTimeoutNanos} to get the timeout factor after which the
62      * device is set off line.
63      */
64     private static final int OFFLINE_TIMEOUT_FACTOR = 10;
65
66     private final Logger logger = LoggerFactory.getLogger(DSMRBridgeHandler.class);
67
68     /**
69      * Additional meter listeners to get received meter values.
70      */
71     private final List<P1TelegramListener> meterListeners = new ArrayList<>();
72
73     /**
74      * Serial Port Manager.
75      */
76     private final SerialPortManager serialPortManager;
77
78     /**
79      * The dsmrDevice managing the connection and handling telegrams.
80      */
81     private @NonNullByDefault({}) DSMRDevice dsmrDevice;
82
83     /**
84      * Long running process that controls the DSMR device connection.
85      */
86     private @NonNullByDefault({}) DSMRDeviceRunnable dsmrDeviceRunnable;
87
88     /**
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.
91      */
92     private @NonNullByDefault({}) Thread dsmrDeviceThread;
93
94     /**
95      * Watchdog to check if messages received and restart if necessary.
96      */
97     private @NonNullByDefault({}) ScheduledFuture<?> watchdog;
98
99     /**
100      * Number of nanoseconds after which a timeout is triggered when no messages received.
101      */
102     private long receivedTimeoutNanos;
103
104     /**
105      * Timestamp in nanoseconds of last P1 telegram received
106      */
107     private volatile long telegramReceivedTimeNanos;
108
109     private final boolean smartyMeter;
110
111     private @Nullable String lastKnownReadErrorMessage;
112
113     /**
114      * Constructor
115      *
116      * @param bridge the Bridge ThingType
117      * @param serialPortManager The Serial port manager
118      */
119     public DSMRBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
120         super(bridge);
121         this.serialPortManager = serialPortManager;
122         smartyMeter = THING_TYPE_SMARTY_BRIDGE.equals(bridge.getThingTypeUID());
123     }
124
125     @Override
126     public Collection<Class<? extends ThingHandlerService>> getServices() {
127         return List.of(DSMRMeterDiscoveryService.class);
128     }
129
130     /**
131      * The {@link DSMRBridgeHandler} does not support handling commands.
132      *
133      * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
134      * @param command the {@link Command}
135      */
136     @Override
137     public void handleCommand(final ChannelUID channelUID, final Command command) {
138         // DSMRBridgeHandler does not support commands
139     }
140
141     /**
142      * Initializes this {@link DSMRBridgeHandler}.
143      *
144      * This method will get the corresponding configuration and initialize and start the corresponding
145      * {@link DSMRDevice}.
146      */
147     @Override
148     public void initialize() {
149         final DSMRDeviceConfiguration deviceConfig = getConfigAs(DSMRDeviceConfiguration.class);
150
151         if (smartyMeter && !validateSmartyMeterConfiguration(deviceConfig)) {
152             return;
153         }
154
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);
168     }
169
170     private boolean validateSmartyMeterConfiguration(final DSMRDeviceConfiguration deviceConfig) {
171         final boolean valid;
172         if (deviceConfig.decryptionKey == null || deviceConfig.decryptionKey.length() != 32) {
173             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
174                     "@text/addon.dsmr.error.configuration.invalidsmartykey");
175             valid = false;
176         } else if (!validDecryptionKey(deviceConfig.decryptionKey, CONFIGURATION_DECRYPTION_KEY)
177                 || !validDecryptionKey(deviceConfig.additionalKey, CONFIGURATION_ADDITIONAL_KEY)) {
178             valid = false;
179         } else {
180             valid = true;
181         }
182         return valid;
183     }
184
185     private boolean validDecryptionKey(final String key, final String message) {
186         try {
187             HexUtils.hexToBytes(key);
188             return true;
189         } catch (final IllegalArgumentException e) {
190             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
191                     "@text/addon.dsmr.error.configuration.invalid." + message + " [" + e.getMessage() + "]");
192         }
193         return false;
194     }
195
196     /**
197      * Creates the {@link DSMRDevice} that corresponds with the user specified configuration.
198      *
199      * @param deviceConfig device configuration
200      * @return Specific {@link DSMRDevice} instance
201      */
202     private DSMRDevice createDevice(final DSMRDeviceConfiguration deviceConfig) {
203         final DSMRDevice dsmrDevice;
204
205         if (smartyMeter) {
206             dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
207                     DSMRSerialSettings.HIGH_SPEED_SETTINGS, this,
208                     new DSMRTelegramListener(deviceConfig.decryptionKey, deviceConfig.additionalKey));
209         } else {
210             final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
211
212             if (deviceConfig.isSerialFixedSettings()) {
213                 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
214                         DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
215             } else {
216                 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
217                         telegramListener, scheduler, deviceConfig.receivedTimeout);
218             }
219         }
220         return dsmrDevice;
221     }
222
223     /**
224      * Registers a meter listener.
225      *
226      * @param meterListener the meter discovery listener to add
227      * @return true if listener is added, false otherwise
228      */
229     public boolean registerDSMRMeterListener(final P1TelegramListener meterListener) {
230         logger.trace("Register DSMRMeterListener");
231         return meterListeners.add(meterListener);
232     }
233
234     /**
235      * Unregisters a meter listener
236      *
237      * @param meterListener the meter discovery listener to remove
238      * @return true is listener is removed, false otherwise
239      */
240     public boolean unregisterDSMRMeterListener(final P1TelegramListener meterListener) {
241         logger.trace("Unregister DSMRMeterListener");
242         return meterListeners.remove(meterListener);
243     }
244
245     /**
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.
250      */
251     private void alive() {
252         logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
253         final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
254
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;
263
264                     deviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, message);
265                 }
266                 resetLastReceivedState();
267             }
268             if (dsmrDeviceRunnable != null) {
269                 dsmrDeviceRunnable.restart();
270             }
271         }
272     }
273
274     /**
275      * Sets the last received time of messages to the current time.
276      */
277     private void resetLastReceivedState() {
278         lastKnownReadErrorMessage = null;
279         telegramReceivedTimeNanos = System.nanoTime();
280         logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
281     }
282
283     @Override
284     public synchronized void telegramReceived(final P1Telegram telegram) {
285         resetLastReceivedState();
286         meterValueReceived(telegram);
287     }
288
289     @Override
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();
294         } else {
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);
300             }
301         }
302     }
303
304     /**
305      * Method to forward the last received messages to the bound meters and to the meterListeners.
306      *
307      * @param telegram received meter values.
308      */
309     private void meterValueReceived(final P1Telegram telegram) {
310         if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
311             updateStatus(ThingStatus.ONLINE);
312         }
313         getThing().getThings().forEach(child -> {
314             if (logger.isTraceEnabled()) {
315                 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
316                         telegram.getCosemObjects().size());
317             }
318             final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
319
320             if (dsmrMeterHandler instanceof DSMRMeterHandler) {
321                 dsmrMeterHandler.telegramReceived(telegram);
322             }
323         });
324         meterListeners.forEach(m -> m.telegramReceived(telegram));
325     }
326
327     @Override
328     public void dispose() {
329         if (watchdog != null) {
330             watchdog.cancel(true);
331             watchdog = null;
332         }
333         if (dsmrDeviceRunnable != null) {
334             dsmrDeviceRunnable.stop();
335         }
336     }
337
338     /**
339      * @param lenientMode the lenientMode to set
340      */
341     public void setLenientMode(final boolean lenientMode) {
342         logger.trace("SetLenientMode: {}", lenientMode);
343         if (dsmrDevice != null) {
344             dsmrDevice.setLenientMode(lenientMode);
345         }
346     }
347
348     /**
349      * Convenience method to set device off line.
350      *
351      * @param status off line status
352      * @param details off line detailed message
353      */
354     private void deviceOffline(final ThingStatusDetail status, final String details) {
355         updateStatus(ThingStatus.OFFLINE, status, details);
356     }
357 }