]> git.basschouten.com Git - openhab-addons.git/blob
d041684a4cb1c29b0f94df06733b50c80f6bc3a3
[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.THING_TYPE_SMARTY_BRIDGE;
16
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;
22
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;
46
47 /**
48  * The {@link DSMRBridgeHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author M. Volaart - Initial contribution
52  * @author Hilbrand Bouwkamp - Refactored way messages are forwarded to meters. Removed availableMeters dependency.
53  */
54 @NonNullByDefault
55 public class DSMRBridgeHandler extends BaseBridgeHandler implements DSMREventListener {
56
57     /**
58      * Factor that will be multiplied with {@link #receivedTimeoutNanos} to get the timeout factor after which the
59      * device is set off line.
60      */
61     private static final int OFFLINE_TIMEOUT_FACTOR = 10;
62
63     private final Logger logger = LoggerFactory.getLogger(DSMRBridgeHandler.class);
64
65     /**
66      * Additional meter listeners to get received meter values.
67      */
68     private final List<P1TelegramListener> meterListeners = new ArrayList<>();
69
70     /**
71      * Serial Port Manager.
72      */
73     private final SerialPortManager serialPortManager;
74
75     /**
76      * The dsmrDevice managing the connection and handling telegrams.
77      */
78     private @NonNullByDefault({}) DSMRDevice dsmrDevice;
79
80     /**
81      * Long running process that controls the DSMR device connection.
82      */
83     private @NonNullByDefault({}) DSMRDeviceRunnable dsmrDeviceRunnable;
84
85     /**
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.
88      */
89     private @NonNullByDefault({}) Thread dsmrDeviceThread;
90
91     /**
92      * Watchdog to check if messages received and restart if necessary.
93      */
94     private @NonNullByDefault({}) ScheduledFuture<?> watchdog;
95
96     /**
97      * Number of nanoseconds after which a timeout is triggered when no messages received.
98      */
99     private long receivedTimeoutNanos;
100
101     /**
102      * Timestamp in nanoseconds of last P1 telegram received
103      */
104     private volatile long telegramReceivedTimeNanos;
105
106     private final boolean smartyMeter;
107
108     /**
109      * Constructor
110      *
111      * @param bridge the Bridge ThingType
112      * @param serialPortManager The Serial port manager
113      */
114     public DSMRBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
115         super(bridge);
116         this.serialPortManager = serialPortManager;
117         smartyMeter = THING_TYPE_SMARTY_BRIDGE.equals(bridge.getThingTypeUID());
118     }
119
120     @Override
121     public Collection<Class<? extends ThingHandlerService>> getServices() {
122         return List.of(DSMRMeterDiscoveryService.class);
123     }
124
125     /**
126      * The {@link DSMRBridgeHandler} does not support handling commands.
127      *
128      * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
129      * @param command the {@link Command}
130      */
131     @Override
132     public void handleCommand(final ChannelUID channelUID, final Command command) {
133         // DSMRBridgeHandler does not support commands
134     }
135
136     /**
137      * Initializes this {@link DSMRBridgeHandler}.
138      *
139      * This method will get the corresponding configuration and initialize and start the corresponding
140      * {@link DSMRDevice}.
141      */
142     @Override
143     public void initialize() {
144         final DSMRDeviceConfiguration deviceConfig = getConfigAs(DSMRDeviceConfiguration.class);
145
146         if (smartyMeter && (deviceConfig.decryptionKey == null || deviceConfig.decryptionKey.length() != 32)) {
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148                     "@text/error.configuration.invalidsmartykey");
149             return;
150         }
151
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);
165     }
166
167     /**
168      * Creates the {@link DSMRDevice} that corresponds with the user specified configuration.
169      *
170      * @param deviceConfig device configuration
171      * @return Specific {@link DSMRDevice} instance
172      */
173     private DSMRDevice createDevice(final DSMRDeviceConfiguration deviceConfig) {
174         final DSMRDevice dsmrDevice;
175
176         if (smartyMeter) {
177             dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
178                     DSMRSerialSettings.HIGH_SPEED_SETTINGS, this,
179                     new DSMRTelegramListener(deviceConfig.decryptionKey, deviceConfig.additionalKey));
180         } else {
181             final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
182
183             if (deviceConfig.isSerialFixedSettings()) {
184                 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
185                         DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
186             } else {
187                 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
188                         telegramListener, scheduler, deviceConfig.receivedTimeout);
189             }
190         }
191         return dsmrDevice;
192     }
193
194     /**
195      * Registers a meter listener.
196      *
197      * @param meterListener the meter discovery listener to add
198      * @return true if listener is added, false otherwise
199      */
200     public boolean registerDSMRMeterListener(final P1TelegramListener meterListener) {
201         logger.trace("Register DSMRMeterListener");
202         return meterListeners.add(meterListener);
203     }
204
205     /**
206      * Unregisters a meter listener
207      *
208      * @param meterListener the meter discovery listener to remove
209      * @return true is listener is removed, false otherwise
210      */
211     public boolean unregisterDSMRMeterListener(final P1TelegramListener meterListener) {
212         logger.trace("Unregister DSMRMeterListener");
213         return meterListeners.remove(meterListener);
214     }
215
216     /**
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.
221      */
222     private void alive() {
223         logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
224         final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
225
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();
231             }
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");
236                 }
237                 resetLastReceivedState();
238             }
239         }
240     }
241
242     /**
243      * Sets the last received time of messages to the current time.
244      */
245     private void resetLastReceivedState() {
246         telegramReceivedTimeNanos = System.nanoTime();
247         logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
248     }
249
250     @Override
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);
256         } else {
257             resetLastReceivedState();
258             meterValueReceived(telegram);
259         }
260     }
261
262     @Override
263     public void handleErrorEvent(final DSMRConnectorErrorEvent portEvent) {
264         if (portEvent != DSMRConnectorErrorEvent.READ_ERROR) {
265             deviceOffline(ThingStatusDetail.CONFIGURATION_ERROR, portEvent.getEventDetails());
266         }
267     }
268
269     /**
270      * Method to forward the last received messages to the bound meters and to the meterListeners.
271      *
272      * @param telegram received meter values.
273      */
274     private void meterValueReceived(final P1Telegram telegram) {
275         if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
276             updateStatus(ThingStatus.ONLINE);
277         }
278         getThing().getThings().forEach(child -> {
279             if (logger.isTraceEnabled()) {
280                 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
281                         telegram.getCosemObjects().size());
282             }
283             final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
284
285             if (dsmrMeterHandler instanceof DSMRMeterHandler) {
286                 dsmrMeterHandler.telegramReceived(telegram);
287             }
288         });
289         meterListeners.forEach(m -> m.telegramReceived(telegram));
290     }
291
292     @Override
293     public void dispose() {
294         if (watchdog != null) {
295             watchdog.cancel(true);
296             watchdog = null;
297         }
298         if (dsmrDeviceRunnable != null) {
299             dsmrDeviceRunnable.stop();
300         }
301     }
302
303     /**
304      * @param lenientMode the lenientMode to set
305      */
306     public void setLenientMode(final boolean lenientMode) {
307         logger.trace("SetLenientMode: {}", lenientMode);
308         if (dsmrDevice != null) {
309             dsmrDevice.setLenientMode(lenientMode);
310         }
311     }
312
313     /**
314      * Convenience method to set device off line.
315      *
316      * @param status off line status
317      * @param details off line detailed message
318      */
319     private void deviceOffline(final ThingStatusDetail status, final String details) {
320         updateStatus(ThingStatus.OFFLINE, status, details);
321     }
322 }