]> git.basschouten.com Git - openhab-addons.git/blob
82ca0957966d4d7b8aacf22059d1e5581958030b
[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.modbus.handler;
14
15 import java.util.List;
16 import java.util.Optional;
17 import java.util.concurrent.CopyOnWriteArrayList;
18 import java.util.concurrent.atomic.AtomicReference;
19 import java.util.stream.Collectors;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.modbus.internal.AtomicStampedValue;
24 import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
25 import org.openhab.binding.modbus.internal.config.ModbusPollerConfiguration;
26 import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
27 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
28 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
29 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
30 import org.openhab.core.io.transport.modbus.ModbusConstants;
31 import org.openhab.core.io.transport.modbus.ModbusFailureCallback;
32 import org.openhab.core.io.transport.modbus.ModbusReadCallback;
33 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
34 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
35 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
36 import org.openhab.core.io.transport.modbus.PollTask;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingStatusInfo;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * The {@link ModbusPollerThingHandler} is responsible for polling Modbus slaves. Errors and data is delegated to
51  * child thing handlers inheriting from {@link ModbusReadCallback} -- in practice: {@link ModbusDataThingHandler}.
52  *
53  * @author Sami Salonen - Initial contribution
54  */
55 @NonNullByDefault
56 public class ModbusPollerThingHandler extends BaseBridgeHandler {
57
58     /**
59      * {@link ModbusReadCallback} that delegates all tasks forward.
60      *
61      * All instances of {@linkplain ReadCallbackDelegator} are considered equal, if they are connected to the same
62      * bridge. This makes sense, as the callback delegates
63      * to all child things of this bridge.
64      *
65      * @author Sami Salonen - Initial contribution
66      *
67      */
68     private class ReadCallbackDelegator
69             implements ModbusReadCallback, ModbusFailureCallback<ModbusReadRequestBlueprint> {
70
71         private volatile @Nullable AtomicStampedValue<PollResult> lastResult;
72
73         public synchronized void handleResult(PollResult result) {
74             // Ignore all incoming data and errors if configuration is not correct
75             if (hasConfigurationError() || disposed) {
76                 return;
77             }
78             if (config.getCacheMillis() >= 0) {
79                 AtomicStampedValue<PollResult> localLastResult = this.lastResult;
80                 if (localLastResult == null) {
81                     this.lastResult = new AtomicStampedValue<>(System.currentTimeMillis(), result);
82                 } else {
83                     localLastResult.update(System.currentTimeMillis(), result);
84                     this.lastResult = localLastResult;
85                 }
86             }
87             logger.debug("Thing {} received response {}", thing.getUID(), result);
88             notifyChildren(result);
89             if (result.failure != null) {
90                 Exception error = result.failure.getCause();
91                 assert error != null;
92                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
93                         String.format("Error with read: %s: %s", error.getClass().getName(), error.getMessage()));
94             } else {
95                 resetCommunicationError();
96             }
97         }
98
99         @Override
100         public synchronized void handle(AsyncModbusReadResult result) {
101             // Casting to allow registers.orElse(null) below..
102             Optional<@Nullable ModbusRegisterArray> registers = (Optional<@Nullable ModbusRegisterArray>) result
103                     .getRegisters();
104             lastPolledDataCache.set(registers.orElse(null));
105             handleResult(new PollResult(result));
106         }
107
108         @Override
109         public synchronized void handle(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
110             handleResult(new PollResult(failure));
111         }
112
113         private void resetCommunicationError() {
114             ThingStatusInfo statusInfo = thing.getStatusInfo();
115             if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
116                     && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
117                 updateStatus(ThingStatus.ONLINE);
118             }
119         }
120
121         /**
122          * Update children data if data is fresh enough
123          *
124          * @param oldestStamp oldest data that is still passed to children
125          * @return whether data was updated. Data is not updated when it's too old or there's no data at all.
126          */
127         @SuppressWarnings("null")
128         public boolean updateChildrenWithOldData(long oldestStamp) {
129             return Optional.ofNullable(this.lastResult).map(result -> result.copyIfStampAfter(oldestStamp))
130                     .map(result -> {
131                         logger.debug("Thing {} reusing cached data: {}", thing.getUID(), result.getValue());
132                         notifyChildren(result.getValue());
133                         return true;
134                     }).orElse(false);
135         }
136
137         private void notifyChildren(PollResult pollResult) {
138             @Nullable
139             AsyncModbusReadResult result = pollResult.result;
140             @Nullable
141             AsyncModbusFailure<ModbusReadRequestBlueprint> failure = pollResult.failure;
142             childCallbacks.forEach(handler -> {
143                 if (result != null) {
144                     handler.onReadResult(result);
145                 } else if (failure != null) {
146                     handler.handleReadError(failure);
147                 }
148             });
149         }
150
151         /**
152          * Rest data caches
153          */
154         public void resetCache() {
155             lastResult = null;
156         }
157     }
158
159     /**
160      * Immutable data object to cache the results of a poll request
161      */
162     private class PollResult {
163
164         public final @Nullable AsyncModbusReadResult result;
165         public final @Nullable AsyncModbusFailure<ModbusReadRequestBlueprint> failure;
166
167         PollResult(AsyncModbusReadResult result) {
168             this.result = result;
169             this.failure = null;
170         }
171
172         PollResult(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
173             this.result = null;
174             this.failure = failure;
175         }
176
177         @Override
178         public String toString() {
179             return failure == null ? String.format("PollResult(result=%s)", result)
180                     : String.format("PollResult(failure=%s)", failure);
181         }
182     }
183
184     private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandler.class);
185
186     private static final List<String> SORTED_READ_FUNCTION_CODES = ModbusBindingConstantsInternal.READ_FUNCTION_CODES
187             .keySet().stream().sorted().collect(Collectors.toUnmodifiableList());
188
189     private @NonNullByDefault({}) ModbusPollerConfiguration config;
190     private long cacheMillis;
191     private volatile @Nullable PollTask pollTask;
192     private volatile @Nullable ModbusReadRequestBlueprint request;
193     private volatile boolean disposed;
194     private volatile List<ModbusDataThingHandler> childCallbacks = new CopyOnWriteArrayList<>();
195     private volatile AtomicReference<@Nullable ModbusRegisterArray> lastPolledDataCache = new AtomicReference<>();
196     private @NonNullByDefault({}) ModbusCommunicationInterface comms;
197
198     private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator();
199
200     private @Nullable ModbusReadFunctionCode functionCode;
201
202     public ModbusPollerThingHandler(Bridge bridge) {
203         super(bridge);
204     }
205
206     @Override
207     public void handleCommand(ChannelUID channelUID, Command command) {
208         // No channels, no commands
209     }
210
211     private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
212         Bridge bridge = getBridge();
213         if (bridge == null) {
214             logger.debug("Bridge is null");
215             return null;
216         }
217         if (bridge.getStatus() != ThingStatus.ONLINE) {
218             logger.debug("Bridge is not online");
219             return null;
220         }
221
222         ThingHandler handler = bridge.getHandler();
223         if (handler == null) {
224             logger.debug("Bridge handler is null");
225             return null;
226         }
227
228         if (handler instanceof ModbusEndpointThingHandler) {
229             ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
230             return slaveEndpoint;
231         } else {
232             logger.debug("Unexpected bridge handler: {}", handler);
233             return null;
234         }
235     }
236
237     @Override
238     public synchronized void initialize() {
239         if (this.getThing().getStatus().equals(ThingStatus.ONLINE)) {
240             // If the bridge was online then first change it to offline.
241             // this ensures that children will be notified about the change
242             updateStatus(ThingStatus.OFFLINE);
243         }
244         this.callbackDelegator.resetCache();
245         comms = null;
246         request = null;
247         disposed = false;
248         logger.trace("Initializing {} from status {}", this.getThing().getUID(), this.getThing().getStatus());
249         try {
250             config = getConfigAs(ModbusPollerConfiguration.class);
251             String type = config.getType();
252             if (!ModbusBindingConstantsInternal.READ_FUNCTION_CODES.containsKey(type)) {
253                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
254                         String.format("No function code found for type='%s'. Was expecting one of: %s", type,
255                                 String.join(", ", SORTED_READ_FUNCTION_CODES)));
256                 return;
257             }
258             functionCode = ModbusBindingConstantsInternal.READ_FUNCTION_CODES.get(type);
259             switch (functionCode) {
260                 case READ_INPUT_REGISTERS:
261                 case READ_MULTIPLE_REGISTERS:
262                     if (config.getLength() > ModbusConstants.MAX_REGISTERS_READ_COUNT) {
263                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
264                                 "Maximum of %d registers can be polled at once due to protocol limitations. Length %d is out of bounds.",
265                                 ModbusConstants.MAX_REGISTERS_READ_COUNT, config.getLength()));
266                         return;
267                     }
268                     break;
269                 case READ_COILS:
270                 case READ_INPUT_DISCRETES:
271                     if (config.getLength() > ModbusConstants.MAX_BITS_READ_COUNT) {
272                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
273                                 "Maximum of %d coils/discrete inputs can be polled at once due to protocol limitations. Length %d is out of bounds.",
274                                 ModbusConstants.MAX_BITS_READ_COUNT, config.getLength()));
275                         return;
276                     }
277                     break;
278             }
279             cacheMillis = this.config.getCacheMillis();
280             registerPollTask();
281         } catch (EndpointNotInitializedException e) {
282             logger.debug("Exception during initialization", e);
283             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
284                     .format("Exception during initialization: %s (%s)", e.getMessage(), e.getClass().getSimpleName()));
285         } finally {
286             logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
287         }
288     }
289
290     @Override
291     public synchronized void dispose() {
292         logger.debug("dispose()");
293         // Mark handler as disposed as soon as possible to halt processing of callbacks
294         disposed = true;
295         unregisterPollTask();
296         this.callbackDelegator.resetCache();
297         comms = null;
298         lastPolledDataCache.set(null);
299     }
300
301     /**
302      * Unregister poll task.
303      *
304      * No-op in case no poll task is registered, or if the initialization is incomplete.
305      */
306     public synchronized void unregisterPollTask() {
307         logger.trace("unregisterPollTask()");
308         if (config == null) {
309             return;
310         }
311         PollTask localPollTask = this.pollTask;
312         if (localPollTask != null) {
313             logger.debug("Unregistering polling from ModbusManager");
314             comms.unregisterRegularPoll(localPollTask);
315         }
316         this.pollTask = null;
317         request = null;
318         comms = null;
319         updateStatus(ThingStatus.OFFLINE);
320     }
321
322     /**
323      * Register poll task
324      *
325      * @throws EndpointNotInitializedException in case the bridge initialization is not complete. This should only
326      *             happen in transient conditions, for example, when bridge is initializing.
327      */
328     @SuppressWarnings("null")
329     private synchronized void registerPollTask() throws EndpointNotInitializedException {
330         logger.trace("registerPollTask()");
331         if (pollTask != null) {
332             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
333             logger.debug("pollTask should be unregistered before registering a new one!");
334             return;
335         }
336
337         ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
338         if (slaveEndpointThingHandler == null) {
339             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format("Bridge '%s' is offline",
340                     Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>")));
341             logger.debug("No bridge handler available -- aborting init for {}", this);
342             return;
343         }
344         ModbusCommunicationInterface localComms = slaveEndpointThingHandler.getCommunicationInterface();
345         if (localComms == null) {
346             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format(
347                     "Bridge '%s' not completely initialized", Optional.ofNullable(getBridge()).map(b -> b.getLabel())));
348             logger.debug("Bridge not initialized fully (no communication interface) -- aborting init for {}", this);
349             return;
350         }
351         this.comms = localComms;
352         ModbusReadFunctionCode localFunctionCode = functionCode;
353         if (localFunctionCode == null) {
354             return;
355         }
356
357         ModbusReadRequestBlueprint localRequest = new ModbusReadRequestBlueprint(slaveEndpointThingHandler.getSlaveId(),
358                 localFunctionCode, config.getStart(), config.getLength(), config.getMaxTries());
359         this.request = localRequest;
360
361         if (config.getRefresh() <= 0L) {
362             logger.debug("Not registering polling with ModbusManager since refresh disabled");
363             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Not polling");
364         } else {
365             logger.debug("Registering polling with ModbusManager");
366             pollTask = localComms.registerRegularPoll(localRequest, config.getRefresh(), 0, callbackDelegator,
367                     callbackDelegator);
368             assert pollTask != null;
369             updateStatus(ThingStatus.ONLINE);
370         }
371     }
372
373     private boolean hasConfigurationError() {
374         ThingStatusInfo statusInfo = getThing().getStatusInfo();
375         return statusInfo.getStatus() == ThingStatus.OFFLINE
376                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
377     }
378
379     @Override
380     public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
381         logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
382         this.dispose();
383         this.initialize();
384     }
385
386     @Override
387     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
388         if (childHandler instanceof ModbusDataThingHandler) {
389             this.childCallbacks.add((ModbusDataThingHandler) childHandler);
390         }
391     }
392
393     @SuppressWarnings("unlikely-arg-type")
394     @Override
395     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
396         if (childHandler instanceof ModbusDataThingHandler) {
397             this.childCallbacks.remove(childHandler);
398         }
399     }
400
401     /**
402      * Return {@link ModbusReadRequestBlueprint} represented by this thing.
403      *
404      * Note that request might be <code>null</code> in case initialization is not complete.
405      *
406      * @return modbus request represented by this poller
407      */
408     public @Nullable ModbusReadRequestBlueprint getRequest() {
409         return request;
410     }
411
412     /**
413      * Get communication interface associated with this poller
414      *
415      * @return
416      */
417     public ModbusCommunicationInterface getCommunicationInterface() {
418         return comms;
419     }
420
421     /**
422      * Refresh the data
423      *
424      * If data or error was just recently received (i.e. cache is fresh), return the cached response.
425      */
426     public void refresh() {
427         ModbusReadRequestBlueprint localRequest = this.request;
428         if (localRequest == null) {
429             return;
430         }
431         ModbusRegisterArray possiblyMutatedCache = lastPolledDataCache.get();
432         AtomicStampedValue<PollResult> lastPollResult = callbackDelegator.lastResult;
433         if (lastPollResult != null && possiblyMutatedCache != null) {
434             AsyncModbusReadResult lastSuccessfulPollResult = lastPollResult.getValue().result;
435             if (lastSuccessfulPollResult != null) {
436                 ModbusRegisterArray lastRegisters = ((Optional<@Nullable ModbusRegisterArray>) lastSuccessfulPollResult
437                         .getRegisters()).orElse(null);
438                 if (lastRegisters != null && !possiblyMutatedCache.equals(lastRegisters)) {
439                     // Register has been mutated in between by a data thing that writes "individual bits"
440                     // Invalidate cache for a fresh poll
441                     callbackDelegator.resetCache();
442                 }
443             }
444         }
445
446         long oldDataThreshold = System.currentTimeMillis() - cacheMillis;
447         boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0
448                 && this.callbackDelegator.updateChildrenWithOldData(oldDataThreshold);
449         if (cacheWasRecentEnoughForUpdate) {
450             logger.debug(
451                     "Poller {} received refresh() and cache was recent enough (age at most {} ms). Reusing old response",
452                     getThing().getUID(), cacheMillis);
453         } else {
454             // cache expired, poll new data
455             logger.debug("Poller {} received refresh() but the cache is not applicable. Polling new data",
456                     getThing().getUID());
457             ModbusCommunicationInterface localComms = comms;
458             if (localComms != null) {
459                 localComms.submitOneTimePoll(localRequest, callbackDelegator, callbackDelegator);
460             }
461         }
462     }
463
464     public AtomicReference<@Nullable ModbusRegisterArray> getLastPolledDataCache() {
465         return lastPolledDataCache;
466     }
467 }