2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.modbus.handler;
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;
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;
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}.
53 * @author Sami Salonen - Initial contribution
56 public class ModbusPollerThingHandler extends BaseBridgeHandler {
59 * {@link ModbusReadCallback} that delegates all tasks forward.
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.
65 * @author Sami Salonen - Initial contribution
68 private class ReadCallbackDelegator
69 implements ModbusReadCallback, ModbusFailureCallback<ModbusReadRequestBlueprint> {
71 private volatile @Nullable AtomicStampedValue<PollResult> lastResult;
73 public synchronized void handleResult(PollResult result) {
74 // Ignore all incoming data and errors if configuration is not correct
75 if (hasConfigurationError() || disposed) {
78 if (config.getCacheMillis() >= 0) {
79 AtomicStampedValue<PollResult> localLastResult = this.lastResult;
80 if (localLastResult == null) {
81 this.lastResult = new AtomicStampedValue<>(System.currentTimeMillis(), result);
83 localLastResult.update(System.currentTimeMillis(), result);
84 this.lastResult = localLastResult;
87 logger.debug("Thing {} received response {}", thing.getUID(), result);
88 notifyChildren(result);
89 if (result.failure != null) {
90 Exception error = result.failure.getCause();
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
93 String.format("Error with read: %s: %s", error.getClass().getName(), error.getMessage()));
95 resetCommunicationError();
100 public synchronized void handle(AsyncModbusReadResult result) {
101 // Casting to allow registers.orElse(null) below..
102 Optional<@Nullable ModbusRegisterArray> registers = (Optional<@Nullable ModbusRegisterArray>) result
104 lastPolledDataCache.set(registers.orElse(null));
105 handleResult(new PollResult(result));
109 public synchronized void handle(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
110 handleResult(new PollResult(failure));
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);
122 * Update children data if data is fresh enough
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.
127 @SuppressWarnings("null")
128 public boolean updateChildrenWithOldData(long oldestStamp) {
129 return Optional.ofNullable(this.lastResult).map(result -> result.copyIfStampAfter(oldestStamp))
131 logger.debug("Thing {} reusing cached data: {}", thing.getUID(), result.getValue());
132 notifyChildren(result.getValue());
137 private void notifyChildren(PollResult pollResult) {
139 AsyncModbusReadResult result = pollResult.result;
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);
154 public void resetCache() {
160 * Immutable data object to cache the results of a poll request
162 private class PollResult {
164 public final @Nullable AsyncModbusReadResult result;
165 public final @Nullable AsyncModbusFailure<ModbusReadRequestBlueprint> failure;
167 PollResult(AsyncModbusReadResult result) {
168 this.result = result;
172 PollResult(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
174 this.failure = failure;
178 public String toString() {
179 return failure == null ? String.format("PollResult(result=%s)", result)
180 : String.format("PollResult(failure=%s)", failure);
184 private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandler.class);
186 private static final List<String> SORTED_READ_FUNCTION_CODES = ModbusBindingConstantsInternal.READ_FUNCTION_CODES
187 .keySet().stream().sorted().collect(Collectors.toUnmodifiableList());
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;
198 private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator();
200 private @Nullable ModbusReadFunctionCode functionCode;
202 public ModbusPollerThingHandler(Bridge bridge) {
207 public void handleCommand(ChannelUID channelUID, Command command) {
208 // No channels, no commands
211 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
212 Bridge bridge = getBridge();
213 if (bridge == null) {
214 logger.debug("Bridge is null");
217 if (bridge.getStatus() != ThingStatus.ONLINE) {
218 logger.debug("Bridge is not online");
222 ThingHandler handler = bridge.getHandler();
223 if (handler == null) {
224 logger.debug("Bridge handler is null");
228 if (handler instanceof ModbusEndpointThingHandler) {
229 ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
230 return slaveEndpoint;
232 logger.debug("Unexpected bridge handler: {}", handler);
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);
244 this.callbackDelegator.resetCache();
248 logger.trace("Initializing {} from status {}", this.getThing().getUID(), this.getThing().getStatus());
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)));
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()));
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()));
279 cacheMillis = this.config.getCacheMillis();
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()));
286 logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
291 public synchronized void dispose() {
292 logger.debug("dispose()");
293 // Mark handler as disposed as soon as possible to halt processing of callbacks
295 unregisterPollTask();
296 this.callbackDelegator.resetCache();
298 lastPolledDataCache.set(null);
302 * Unregister poll task.
304 * No-op in case no poll task is registered, or if the initialization is incomplete.
306 public synchronized void unregisterPollTask() {
307 logger.trace("unregisterPollTask()");
308 if (config == null) {
311 PollTask localPollTask = this.pollTask;
312 if (localPollTask != null) {
313 logger.debug("Unregistering polling from ModbusManager");
314 comms.unregisterRegularPoll(localPollTask);
316 this.pollTask = null;
319 updateStatus(ThingStatus.OFFLINE);
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.
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!");
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);
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);
351 this.comms = localComms;
352 ModbusReadFunctionCode localFunctionCode = functionCode;
353 if (localFunctionCode == null) {
357 ModbusReadRequestBlueprint localRequest = new ModbusReadRequestBlueprint(slaveEndpointThingHandler.getSlaveId(),
358 localFunctionCode, config.getStart(), config.getLength(), config.getMaxTries());
359 this.request = localRequest;
361 if (config.getRefresh() <= 0L) {
362 logger.debug("Not registering polling with ModbusManager since refresh disabled");
363 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Not polling");
365 logger.debug("Registering polling with ModbusManager");
366 pollTask = localComms.registerRegularPoll(localRequest, config.getRefresh(), 0, callbackDelegator,
368 assert pollTask != null;
369 updateStatus(ThingStatus.ONLINE);
373 private boolean hasConfigurationError() {
374 ThingStatusInfo statusInfo = getThing().getStatusInfo();
375 return statusInfo.getStatus() == ThingStatus.OFFLINE
376 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
380 public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
381 logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
387 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
388 if (childHandler instanceof ModbusDataThingHandler) {
389 this.childCallbacks.add((ModbusDataThingHandler) childHandler);
393 @SuppressWarnings("unlikely-arg-type")
395 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
396 if (childHandler instanceof ModbusDataThingHandler) {
397 this.childCallbacks.remove(childHandler);
402 * Return {@link ModbusReadRequestBlueprint} represented by this thing.
404 * Note that request might be <code>null</code> in case initialization is not complete.
406 * @return modbus request represented by this poller
408 public @Nullable ModbusReadRequestBlueprint getRequest() {
413 * Get communication interface associated with this poller
417 public ModbusCommunicationInterface getCommunicationInterface() {
424 * If data or error was just recently received (i.e. cache is fresh), return the cached response.
426 public void refresh() {
427 ModbusReadRequestBlueprint localRequest = this.request;
428 if (localRequest == null) {
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();
446 long oldDataThreshold = System.currentTimeMillis() - cacheMillis;
447 boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0
448 && this.callbackDelegator.updateChildrenWithOldData(oldDataThreshold);
449 if (cacheWasRecentEnoughForUpdate) {
451 "Poller {} received refresh() and cache was recent enough (age at most {} ms). Reusing old response",
452 getThing().getUID(), cacheMillis);
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);
464 public AtomicReference<@Nullable ModbusRegisterArray> getLastPolledDataCache() {
465 return lastPolledDataCache;