]> git.basschouten.com Git - openhab-addons.git/blob
12c74c572dd7c969826ebdd8ea8455881869151d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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(Bridge bridge, 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(ChannelUID channelUID, 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(DSMRDeviceConfiguration deviceConfig) {
174         final DSMRDevice dsmrDevice;
175
176         if (smartyMeter) {
177             dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
178                     DSMRSerialSettings.HIGH_SPEED_SETTINGS, this, new DSMRTelegramListener(deviceConfig.decryptionKey));
179         } else {
180             final DSMRTelegramListener telegramListener = new DSMRTelegramListener();
181
182             if (deviceConfig.isSerialFixedSettings()) {
183                 dsmrDevice = new DSMRFixedConfigDevice(serialPortManager, deviceConfig.serialPort,
184                         DSMRSerialSettings.getPortSettingsFromConfiguration(deviceConfig), this, telegramListener);
185             } else {
186                 dsmrDevice = new DSMRSerialAutoDevice(serialPortManager, deviceConfig.serialPort, this,
187                         telegramListener, scheduler, deviceConfig.receivedTimeout);
188             }
189         }
190         return dsmrDevice;
191     }
192
193     /**
194      * Registers a meter listener.
195      *
196      * @param meterListener the meter discovery listener to add
197      * @return true if listener is added, false otherwise
198      */
199     public boolean registerDSMRMeterListener(P1TelegramListener meterListener) {
200         logger.trace("Register DSMRMeterListener");
201         return meterListeners.add(meterListener);
202     }
203
204     /**
205      * Unregisters a meter listener
206      *
207      * @param meterListener the meter discovery listener to remove
208      * @return true is listener is removed, false otherwise
209      */
210     public boolean unregisterDSMRMeterListener(P1TelegramListener meterListener) {
211         logger.trace("Unregister DSMRMeterListener");
212         return meterListeners.remove(meterListener);
213     }
214
215     /**
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.
220      */
221     private void alive() {
222         logger.trace("Bridge alive check with #{} children.", getThing().getThings().size());
223         final long deltaLastReceived = System.nanoTime() - telegramReceivedTimeNanos;
224
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();
230             }
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");
235                 }
236                 resetLastReceivedState();
237             }
238         }
239     }
240
241     /**
242      * Sets the last received time of messages to the current time.
243      */
244     private void resetLastReceivedState() {
245         telegramReceivedTimeNanos = System.nanoTime();
246         logger.trace("Telegram received time set: {}", telegramReceivedTimeNanos);
247     }
248
249     @Override
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);
255         } else {
256             resetLastReceivedState();
257             meterValueReceived(telegram);
258         }
259     }
260
261     @Override
262     public void handleErrorEvent(DSMRConnectorErrorEvent portEvent) {
263         if (portEvent != DSMRConnectorErrorEvent.READ_ERROR) {
264             deviceOffline(ThingStatusDetail.CONFIGURATION_ERROR, portEvent.getEventDetails());
265         }
266     }
267
268     /**
269      * Method to forward the last received messages to the bound meters and to the meterListeners.
270      *
271      * @param telegram received meter values.
272      */
273     private void meterValueReceived(P1Telegram telegram) {
274         if (isInitialized() && getThing().getStatus() != ThingStatus.ONLINE) {
275             updateStatus(ThingStatus.ONLINE);
276         }
277         getThing().getThings().forEach(child -> {
278             if (logger.isTraceEnabled()) {
279                 logger.trace("Update child:{} with {} objects", child.getThingTypeUID().getId(),
280                         telegram.getCosemObjects().size());
281             }
282             final DSMRMeterHandler dsmrMeterHandler = (DSMRMeterHandler) child.getHandler();
283
284             if (dsmrMeterHandler instanceof DSMRMeterHandler) {
285                 dsmrMeterHandler.telegramReceived(telegram);
286             }
287         });
288         meterListeners.forEach(m -> m.telegramReceived(telegram));
289     }
290
291     @Override
292     public void dispose() {
293         if (watchdog != null) {
294             watchdog.cancel(true);
295             watchdog = null;
296         }
297         if (dsmrDeviceRunnable != null) {
298             dsmrDeviceRunnable.stop();
299         }
300     }
301
302     /**
303      * @param lenientMode the lenientMode to set
304      */
305     public void setLenientMode(boolean lenientMode) {
306         logger.trace("SetLenientMode: {}", lenientMode);
307         if (dsmrDevice != null) {
308             dsmrDevice.setLenientMode(lenientMode);
309         }
310     }
311
312     /**
313      * Convenience method to set device off line.
314      *
315      * @param status off line status
316      * @param details off line detailed message
317      */
318     private void deviceOffline(ThingStatusDetail status, String details) {
319         updateStatus(ThingStatus.OFFLINE, status, details);
320     }
321 }