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