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