2 * Copyright (c) 2010-2021 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.internal.handler;
15 import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
17 import java.math.BigDecimal;
18 import java.time.Duration;
19 import java.time.LocalDateTime;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.Objects;
26 import java.util.Optional;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
32 import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
33 import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
34 import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
35 import org.openhab.binding.modbus.internal.ModbusConfigurationException;
36 import org.openhab.binding.modbus.internal.Transformation;
37 import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration;
38 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
39 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
40 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
41 import org.openhab.core.io.transport.modbus.BitArray;
42 import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
43 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
44 import org.openhab.core.io.transport.modbus.ModbusConstants;
45 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
46 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
47 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
48 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
49 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
50 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
51 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
52 import org.openhab.core.io.transport.modbus.exception.ModbusConnectionException;
53 import org.openhab.core.io.transport.modbus.exception.ModbusTransportException;
54 import org.openhab.core.io.transport.modbus.json.WriteRequestJsonUtilities;
55 import org.openhab.core.library.items.ContactItem;
56 import org.openhab.core.library.items.DateTimeItem;
57 import org.openhab.core.library.items.DimmerItem;
58 import org.openhab.core.library.items.NumberItem;
59 import org.openhab.core.library.items.RollershutterItem;
60 import org.openhab.core.library.items.StringItem;
61 import org.openhab.core.library.items.SwitchItem;
62 import org.openhab.core.library.types.DateTimeType;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.OpenClosedType;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.ThingStatusInfo;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.BridgeHandler;
74 import org.openhab.core.thing.binding.ThingHandlerCallback;
75 import org.openhab.core.types.Command;
76 import org.openhab.core.types.RefreshType;
77 import org.openhab.core.types.State;
78 import org.openhab.core.types.UnDefType;
79 import org.osgi.framework.BundleContext;
80 import org.osgi.framework.FrameworkUtil;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
85 * The {@link ModbusDataThingHandler} is responsible for interpreting polled modbus data, as well as handling openHAB
88 * Thing can be re-initialized by the bridge in case of configuration changes (bridgeStatusChanged).
89 * Because of this, initialize, dispose and all callback methods (onRegisters, onBits, onError, onWriteResponse) are
91 * to avoid data race conditions.
93 * @author Sami Salonen - Initial contribution
96 public class ModbusDataThingHandler extends BaseThingHandler {
98 private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class);
100 private final BundleContext bundleContext;
102 private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
103 private static final Map<String, List<Class<? extends State>>> CHANNEL_ID_TO_ACCEPTED_TYPES = new HashMap<>();
106 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_SWITCH,
107 new SwitchItem("").getAcceptedDataTypes());
108 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_CONTACT,
109 new ContactItem("").getAcceptedDataTypes());
110 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DATETIME,
111 new DateTimeItem("").getAcceptedDataTypes());
112 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DIMMER,
113 new DimmerItem("").getAcceptedDataTypes());
114 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_NUMBER,
115 new NumberItem("").getAcceptedDataTypes());
116 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_STRING,
117 new StringItem("").getAcceptedDataTypes());
118 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_ROLLERSHUTTER,
119 new RollershutterItem("").getAcceptedDataTypes());
121 // data channels + 4 for read/write last error/success
122 private static final int NUMER_OF_CHANNELS_HINT = CHANNEL_ID_TO_ACCEPTED_TYPES.size() + 4;
125 // If you change the below default/initial values, please update the corresponding values in dispose()
127 private volatile @Nullable ModbusDataConfiguration config;
128 private volatile @Nullable ValueType readValueType;
129 private volatile @Nullable ValueType writeValueType;
130 private volatile @Nullable Transformation readTransformation;
131 private volatile @Nullable Transformation writeTransformation;
132 private volatile Optional<Integer> readIndex = Optional.empty();
133 private volatile Optional<Integer> readSubIndex = Optional.empty();
134 private volatile @Nullable Integer writeStart;
135 private volatile int pollStart;
136 private volatile int slaveId;
137 private volatile @Nullable ModbusReadFunctionCode functionCode;
138 private volatile @Nullable ModbusReadRequestBlueprint readRequest;
139 private volatile long updateUnchangedValuesEveryMillis;
140 private volatile @NonNullByDefault({}) ModbusCommunicationInterface comms;
141 private volatile boolean isWriteEnabled;
142 private volatile boolean isReadEnabled;
143 private volatile boolean writeParametersHavingTransformationOnly;
144 private volatile boolean childOfEndpoint;
145 private volatile @Nullable ModbusPollerThingHandler pollerHandler;
146 private volatile Map<String, ChannelUID> channelCache = new HashMap<>();
147 private volatile Map<ChannelUID, Long> channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
148 private volatile Map<ChannelUID, State> channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
150 private volatile LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
151 private volatile ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
154 public ModbusDataThingHandler(Thing thing) {
156 this.bundleContext = FrameworkUtil.getBundle(ModbusDataThingHandler.class).getBundleContext();
160 public synchronized void handleCommand(ChannelUID channelUID, Command command) {
161 logger.trace("Thing {} '{}' received command '{}' to channel '{}'", getThing().getUID(), getThing().getLabel(),
162 command, channelUID);
163 ModbusDataConfiguration config = this.config;
164 if (config == null) {
168 if (RefreshType.REFRESH == command) {
169 ModbusPollerThingHandler poller = pollerHandler;
170 if (poller == null) {
171 // Data thing must be child of endpoint, and thus write-only.
172 // There is no data to update
175 // We *schedule* the REFRESH to avoid dead-lock situation where poller is trying update this
176 // data thing with cached data (resulting in deadlock in two synchronized methods: this (handleCommand) and
178 scheduler.schedule(() -> poller.refresh(), 0, TimeUnit.SECONDS);
180 } else if (hasConfigurationError()) {
182 "Thing {} '{}' command '{}' to channel '{}': Thing has configuration error so ignoring the command",
183 getThing().getUID(), getThing().getLabel(), command, channelUID);
185 } else if (!isWriteEnabled) {
187 "Thing {} '{}' command '{}' to channel '{}': no writing configured -> aborting processing command",
188 getThing().getUID(), getThing().getLabel(), command, channelUID);
192 Optional<Command> transformedCommand = transformCommandAndProcessJSON(channelUID, command);
193 if (transformedCommand == null) {
194 // We have, JSON as transform output (which has been processed) or some error. See
195 // transformCommandAndProcessJSON javadoc
199 // We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
201 Integer writeStart = this.writeStart;
202 if (writeStart == null) {
204 "Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
205 getThing().getUID(), getThing().getLabel(), command);
209 if (!transformedCommand.isPresent()) {
210 // transformation failed, return
211 logger.warn("Cannot process command {} (of type {}) with channel {} since transformation was unsuccessful",
212 command, command.getClass().getSimpleName(), channelUID);
216 ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
218 if (request == null) {
222 logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint());
223 comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
227 * Transform received command using the transformation.
229 * In case of JSON as transformation output, the output processed using {@link processJsonTransform}.
231 * @param channelUID channel UID corresponding to received command
232 * @param command command to be transformed
233 * @return transformed command. Null is returned with JSON transformation outputs and configuration errors
235 * @see processJsonTransform
237 private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
238 String transformOutput;
239 Optional<Command> transformedCommand;
240 Transformation writeTransformation = this.writeTransformation;
241 if (writeTransformation == null || writeTransformation.isIdentityTransform()) {
242 transformedCommand = Optional.of(command);
244 transformOutput = writeTransformation.transform(bundleContext, command.toString());
245 if (transformOutput.contains("[")) {
246 processJsonTransform(command, transformOutput);
248 } else if (writeParametersHavingTransformationOnly) {
249 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
250 "Seems to have writeTransformation but no other write parameters. Since the transformation did not return a JSON for command '%s' (channel %s), this is a configuration error",
251 command, channelUID));
254 transformedCommand = Transformation.tryConvertToCommand(transformOutput);
255 logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput,
256 transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"),
257 transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
260 return transformedCommand;
263 private @Nullable ModbusWriteRequestBlueprint requestFromCommand(ChannelUID channelUID, Command origCommand,
264 ModbusDataConfiguration config, Command transformedCommand, Integer writeStart) {
265 ModbusWriteRequestBlueprint request;
266 boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
267 String writeType = config.getWriteType();
268 if (writeType == null) {
271 if (writeType.equals(WRITE_TYPE_COIL)) {
272 Optional<Boolean> commandAsBoolean = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
273 if (!commandAsBoolean.isPresent()) {
275 "Cannot process command {} with channel {} since command is not OnOffType, OpenClosedType or Decimal trying to write to coil. Do not know how to convert to 0/1. Transformed command was '{}'",
276 origCommand, channelUID, transformedCommand);
279 boolean data = commandAsBoolean.get();
280 request = new ModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple,
281 config.getWriteMaxTries());
282 } else if (writeType.equals(WRITE_TYPE_HOLDING)) {
283 ValueType writeValueType = this.writeValueType;
284 if (writeValueType == null) {
285 // Should not happen in practice, since we are not in configuration error (checked above)
286 // This will make compiler happy anyways with the null checks
287 logger.warn("Received command but write value type not set! Ignoring command");
290 ModbusRegisterArray data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
291 writeMultiple = writeMultiple || data.size() > 1;
292 request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
293 config.getWriteMaxTries());
295 // Should not happen! This method is not called in case configuration errors and writeType is validated
296 // already in initialization (validateAndParseWriteParameters).
297 // We keep this here for future-proofing the code (new writeType values)
298 throw new IllegalStateException(String.format(
299 "writeType does not equal %s or %s and thus configuration is invalid. Should not end up this far with configuration error.",
300 WRITE_TYPE_COIL, WRITE_TYPE_HOLDING));
305 private void processJsonTransform(Command command, String transformOutput) {
306 ModbusCommunicationInterface localComms = this.comms;
307 if (localComms == null) {
310 Collection<ModbusWriteRequestBlueprint> requests;
312 requests = WriteRequestJsonUtilities.fromJson(slaveId, transformOutput);
313 } catch (IllegalArgumentException | IllegalStateException e) {
315 "Thing {} '{}' could handle transformation result '{}'. Original command {}. Error details follow",
316 getThing().getUID(), getThing().getLabel(), transformOutput, command, e);
320 requests.stream().forEach(request -> {
321 logger.trace("Submitting write request: {} to endpoint {} (based from transformation {})", request,
322 localComms.getEndpoint(), transformOutput);
323 localComms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
328 public synchronized void initialize() {
329 // Initialize the thing. If done set status to ONLINE to indicate proper working.
330 // Long running initialization should be done asynchronously in background.
332 logger.trace("initialize() of thing {} '{}' starting", thing.getUID(), thing.getLabel());
333 ModbusDataConfiguration localConfig = config = getConfigAs(ModbusDataConfiguration.class);
334 updateUnchangedValuesEveryMillis = localConfig.getUpdateUnchangedValuesEveryMillis();
335 Bridge bridge = getBridge();
336 if (bridge == null || !bridge.getStatus().equals(ThingStatus.ONLINE)) {
337 logger.debug("Thing {} '{}' has no bridge or it is not online", getThing().getUID(),
338 getThing().getLabel());
339 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No online bridge");
342 BridgeHandler bridgeHandler = bridge.getHandler();
343 if (bridgeHandler == null) {
344 logger.warn("Bridge {} '{}' has no handler.", bridge.getUID(), bridge.getLabel());
345 String errmsg = String.format("Bridge %s '%s' configuration incomplete or with errors", bridge.getUID(),
347 throw new ModbusConfigurationException(errmsg);
349 if (bridgeHandler instanceof ModbusEndpointThingHandler) {
350 // Write-only thing, parent is endpoint
351 ModbusEndpointThingHandler endpointHandler = (ModbusEndpointThingHandler) bridgeHandler;
352 slaveId = endpointHandler.getSlaveId();
353 comms = endpointHandler.getCommunicationInterface();
354 childOfEndpoint = true;
358 ModbusPollerThingHandler localPollerHandler = (ModbusPollerThingHandler) bridgeHandler;
359 pollerHandler = localPollerHandler;
360 ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest();
361 if (localReadRequest == null) {
363 "Poller {} '{}' has no read request -- configuration is changing or bridge having invalid configuration?",
364 bridge.getUID(), bridge.getLabel());
365 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
366 String.format("Poller %s '%s' has no poll task", bridge.getUID(), bridge.getLabel()));
369 readRequest = localReadRequest;
370 slaveId = localReadRequest.getUnitID();
371 functionCode = localReadRequest.getFunctionCode();
372 comms = localPollerHandler.getCommunicationInterface();
373 pollStart = localReadRequest.getReference();
374 childOfEndpoint = false;
376 validateAndParseReadParameters(localConfig);
377 validateAndParseWriteParameters(localConfig);
378 validateMustReadOrWrite();
380 updateStatusIfChanged(ThingStatus.ONLINE);
381 } catch (ModbusConfigurationException | EndpointNotInitializedException e) {
382 logger.debug("Thing {} '{}' initialization error: {}", getThing().getUID(), getThing().getLabel(),
384 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
386 logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
391 public synchronized void dispose() {
393 readValueType = null;
394 writeValueType = null;
395 readTransformation = null;
396 writeTransformation = null;
397 readIndex = Optional.empty();
398 readSubIndex = Optional.empty();
405 isWriteEnabled = false;
406 isReadEnabled = false;
407 writeParametersHavingTransformationOnly = false;
408 childOfEndpoint = false;
409 pollerHandler = null;
410 channelCache = new HashMap<>();
411 lastStatusInfoUpdate = LocalDateTime.MIN;
412 statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null);
413 channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
414 channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
418 public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
419 logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
424 private boolean hasConfigurationError() {
425 ThingStatusInfo statusInfo = getThing().getStatusInfo();
426 return statusInfo.getStatus() == ThingStatus.OFFLINE
427 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
430 private void validateMustReadOrWrite() throws ModbusConfigurationException {
431 if (!isReadEnabled && !isWriteEnabled) {
432 throw new ModbusConfigurationException("Should try to read or write data!");
436 private void validateAndParseReadParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
437 ModbusReadFunctionCode functionCode = this.functionCode;
438 boolean readingDiscreteOrCoil = functionCode == ModbusReadFunctionCode.READ_COILS
439 || functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES;
440 boolean readStartMissing = config.getReadStart() == null || config.getReadStart().isBlank();
441 boolean readValueTypeMissing = config.getReadValueType() == null || config.getReadValueType().isBlank();
443 if (childOfEndpoint && readRequest == null) {
444 if (!readStartMissing || !readValueTypeMissing) {
445 String errmsg = String.format(
446 "Thing %s readStart=%s, and readValueType=%s were specified even though the data thing is child of endpoint (that is, write-only)!",
447 getThing().getUID(), config.getReadStart(), config.getReadValueType());
448 throw new ModbusConfigurationException(errmsg);
452 // we assume readValueType=bit by default if it is missing
453 boolean allMissingOrAllPresent = (readStartMissing && readValueTypeMissing)
454 || (!readStartMissing && (!readValueTypeMissing || readingDiscreteOrCoil));
455 if (!allMissingOrAllPresent) {
456 String errmsg = String.format(
457 "Thing %s readStart=%s, and readValueType=%s should be all present or all missing!",
458 getThing().getUID(), config.getReadStart(), config.getReadValueType());
459 throw new ModbusConfigurationException(errmsg);
460 } else if (!readStartMissing) {
461 // all read values are present
462 isReadEnabled = true;
463 if (readingDiscreteOrCoil && readValueTypeMissing) {
464 readValueType = ModbusConstants.ValueType.BIT;
467 readValueType = ValueType.fromConfigValue(config.getReadValueType());
468 } catch (IllegalArgumentException e) {
469 String errmsg = String.format("Thing %s readValueType=%s is invalid!", getThing().getUID(),
470 config.getReadValueType());
471 throw new ModbusConfigurationException(errmsg);
475 if (readingDiscreteOrCoil && !ModbusConstants.ValueType.BIT.equals(readValueType)) {
476 String errmsg = String.format(
477 "Thing %s invalid readValueType: Only readValueType='%s' (or undefined) supported with coils or discrete inputs. Value type was: %s",
478 getThing().getUID(), ModbusConstants.ValueType.BIT, config.getReadValueType());
479 throw new ModbusConfigurationException(errmsg);
482 isReadEnabled = false;
486 String readStart = config.getReadStart();
487 if (readStart == null) {
488 throw new ModbusConfigurationException(
489 String.format("Thing %s invalid readStart: %s", getThing().getUID(), config.getReadStart()));
491 String[] readParts = readStart.split("\\.", 2);
493 readIndex = Optional.of(Integer.parseInt(readParts[0]));
494 if (readParts.length == 2) {
495 readSubIndex = Optional.of(Integer.parseInt(readParts[1]));
497 readSubIndex = Optional.empty();
499 } catch (IllegalArgumentException e) {
500 String errmsg = String.format("Thing %s invalid readStart: %s", getThing().getUID(),
501 config.getReadStart());
502 throw new ModbusConfigurationException(errmsg);
505 readTransformation = new Transformation(config.getReadTransform());
509 private void validateAndParseWriteParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
510 boolean writeTypeMissing = config.getWriteType() == null || config.getWriteType().isBlank();
511 boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank();
512 boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().isBlank();
513 boolean writeTransformationMissing = config.getWriteTransform() == null || config.getWriteTransform().isBlank();
514 writeTransformation = new Transformation(config.getWriteTransform());
515 boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType());
516 writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing
517 && !writeTransformationMissing);
518 boolean allMissingOrAllPresentOrOnlyNonDefaultTransform = //
519 // read-only thing, no write specified
520 (writeTypeMissing && writeStartMissing && writeValueTypeMissing)
521 // mandatory write parameters provided. With coils one can drop value type
522 || (!writeTypeMissing && !writeStartMissing && (!writeValueTypeMissing || writingCoil))
523 // only transformation provided
524 || writeParametersHavingTransformationOnly;
525 if (!allMissingOrAllPresentOrOnlyNonDefaultTransform) {
526 String errmsg = String.format(
527 "writeType=%s, writeStart=%s, and writeValueType=%s should be all present, or all missing! Alternatively, you can provide just writeTransformation, and use transformation returning JSON.",
528 config.getWriteType(), config.getWriteStart(), config.getWriteValueType());
529 throw new ModbusConfigurationException(errmsg);
530 } else if (!writeTypeMissing || writeParametersHavingTransformationOnly) {
531 isWriteEnabled = true;
532 // all write values are present
533 if (!writeParametersHavingTransformationOnly && !WRITE_TYPE_HOLDING.equals(config.getWriteType())
534 && !WRITE_TYPE_COIL.equals(config.getWriteType())) {
535 String errmsg = String.format("Invalid writeType=%s. Expecting %s or %s!", config.getWriteType(),
536 WRITE_TYPE_HOLDING, WRITE_TYPE_COIL);
537 throw new ModbusConfigurationException(errmsg);
539 final ValueType localWriteValueType;
540 if (writeParametersHavingTransformationOnly) {
541 // Placeholder for further checks
542 localWriteValueType = writeValueType = ModbusConstants.ValueType.INT16;
543 } else if (writingCoil && writeValueTypeMissing) {
544 localWriteValueType = writeValueType = ModbusConstants.ValueType.BIT;
547 localWriteValueType = writeValueType = ValueType.fromConfigValue(config.getWriteValueType());
548 } catch (IllegalArgumentException e) {
549 String errmsg = String.format("Invalid writeValueType=%s!", config.getWriteValueType());
550 throw new ModbusConfigurationException(errmsg);
554 if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
555 String errmsg = String.format(
556 "Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
557 ModbusConstants.ValueType.BIT, config.getWriteValueType());
558 throw new ModbusConfigurationException(errmsg);
559 } else if (!writingCoil && localWriteValueType.getBits() < 16) {
560 // trying to write holding registers with < 16 bit value types. Not supported
561 String errmsg = String.format(
562 "Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
563 config.getWriteValueType());
564 throw new ModbusConfigurationException(errmsg);
568 if (!writeParametersHavingTransformationOnly) {
569 String localWriteStart = config.getWriteStart();
570 if (localWriteStart == null) {
571 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
572 config.getWriteStart());
573 throw new ModbusConfigurationException(errmsg);
575 writeStart = Integer.parseInt(localWriteStart.trim());
577 } catch (IllegalArgumentException e) {
578 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
579 config.getWriteStart());
580 throw new ModbusConfigurationException(errmsg);
583 isWriteEnabled = false;
587 private void validateReadIndex() throws ModbusConfigurationException {
589 ModbusReadRequestBlueprint readRequest = this.readRequest;
590 ValueType readValueType = this.readValueType;
591 if (!readIndex.isPresent() || readRequest == null) {
594 assert readValueType != null;
595 // bits represented by the value type, e.g. int32 -> 32
596 int valueTypeBitCount = readValueType.getBits();
598 switch (readRequest.getFunctionCode()) {
599 case READ_INPUT_REGISTERS:
600 case READ_MULTIPLE_REGISTERS:
601 dataElementBits = 16;
604 case READ_INPUT_DISCRETES:
608 throw new IllegalStateException(readRequest.getFunctionCode().toString());
611 boolean bitQuery = dataElementBits == 1;
612 if (bitQuery && readSubIndex.isPresent()) {
613 String errmsg = String.format("readStart=X.Y is not allowed to be used with coils or discrete inputs!");
614 throw new ModbusConfigurationException(errmsg);
617 if (valueTypeBitCount >= 16 && readSubIndex.isPresent()) {
618 String errmsg = String
619 .format("readStart=X.Y is not allowed to be used with value types larger than 16bit!");
620 throw new ModbusConfigurationException(errmsg);
621 } else if (!bitQuery && valueTypeBitCount < 16 && !readSubIndex.isPresent()) {
622 String errmsg = String.format("readStart=X.Y must be used with value types less than 16bit!");
623 throw new ModbusConfigurationException(errmsg);
624 } else if (readSubIndex.isPresent() && (readSubIndex.get() + 1) * valueTypeBitCount > 16) {
625 // the sub index Y (in X.Y) is above the register limits
626 String errmsg = String.format("readStart=X.Y, the value Y is too large");
627 throw new ModbusConfigurationException(errmsg);
630 // Determine bit positions polled, both start and end inclusive
631 int pollStartBitIndex = readRequest.getReference() * dataElementBits;
632 int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits - 1;
634 // Determine bit positions read, both start and end inclusive
635 int readStartBitIndex = readIndex.get() * dataElementBits + readSubIndex.orElse(0) * valueTypeBitCount;
636 int readEndBitIndex = readStartBitIndex + valueTypeBitCount - 1;
638 if (readStartBitIndex < pollStartBitIndex || readEndBitIndex > pollEndBitIndex) {
639 String errmsg = String.format(
640 "Out-of-bounds: Poller is reading from index %d to %d (inclusive) but this thing configured to read '%s' starting from element %d. Exceeds polled data bounds.",
641 pollStartBitIndex / dataElementBits, pollEndBitIndex / dataElementBits, readValueType,
643 throw new ModbusConfigurationException(errmsg);
647 private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
648 return channelAcceptedDataTypes.stream().anyMatch(clz -> {
649 return clz.equals(OnOffType.class);
653 private boolean containsOpenClosed(List<Class<? extends State>> acceptedDataTypes) {
654 return acceptedDataTypes.stream().anyMatch(clz -> {
655 return clz.equals(OpenClosedType.class);
659 public synchronized void onReadResult(AsyncModbusReadResult result) {
660 result.getRegisters().ifPresent(registers -> onRegisters(result.getRequest(), registers));
661 result.getBits().ifPresent(bits -> onBits(result.getRequest(), bits));
664 public synchronized void handleReadError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
665 onError(failure.getRequest(), failure.getCause());
668 public synchronized void handleWriteError(AsyncModbusFailure<ModbusWriteRequestBlueprint> failure) {
669 onError(failure.getRequest(), failure.getCause());
672 private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
673 if (hasConfigurationError()) {
675 } else if (!isReadEnabled) {
678 ValueType readValueType = this.readValueType;
679 if (readValueType == null) {
685 // e.g. with bit, extractIndex=4 means 5th bit (from right) ("10.4" -> 5th bit of register 10, "10.4" -> 5th bit
687 // bit of second register)
688 // e.g. with 8bit integer, extractIndex=3 means high byte of second register
690 // with <16 bit types, this is the index of the N'th 1-bit/8-bit item. Each register has 16/2 items,
692 // with >=16 bit types, this is index of first register
694 if (readValueType.getBits() >= 16) {
695 // Invariant, checked in initialize
696 assert readSubIndex.orElse(0) == 0;
697 extractIndex = readIndex.get() - pollStart;
699 int subIndex = readSubIndex.orElse(0);
700 int itemsPerRegister = 16 / readValueType.getBits();
701 extractIndex = (readIndex.get() - pollStart) * itemsPerRegister + subIndex;
703 numericState = ModbusBitUtilities.extractStateFromRegisters(registers, extractIndex, readValueType)
704 .map(state -> (State) state).orElse(UnDefType.UNDEF);
705 boolean boolValue = !numericState.equals(DecimalType.ZERO);
706 Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
708 "Thing {} channels updated: {}. readValueType={}, readIndex={}, readSubIndex(or 0)={}, extractIndex={} -> numeric value {} and boolValue={}. Registers {} for request {}",
709 thing.getUID(), values, readValueType, readIndex, readSubIndex.orElse(0), extractIndex, numericState,
710 boolValue, registers, request);
713 private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
714 if (hasConfigurationError()) {
716 } else if (!isReadEnabled) {
719 boolean boolValue = bits.getBit(readIndex.get() - pollStart);
720 DecimalType numericState = boolValue ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO;
721 Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
723 "Thing {} channels updated: {}. readValueType={}, readIndex={} -> numeric value {} and boolValue={}. Bits {} for request {}",
724 thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request);
727 private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) {
728 if (hasConfigurationError()) {
730 } else if (!isReadEnabled) {
733 if (error instanceof ModbusConnectionException) {
734 logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
735 error.getClass().getSimpleName(), error.toString());
736 } else if (error instanceof ModbusTransportException) {
737 logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
738 error.getClass().getSimpleName(), error.toString());
741 "Thing {} '{}' had {} error on read: {} (message: {}). Stack trace follows since this is unexpected error.",
742 getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
743 error.getMessage(), error);
745 Map<ChannelUID, State> states = new HashMap<>();
746 ChannelUID lastReadErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_ERROR);
747 if (isLinked(lastReadErrorUID)) {
748 states.put(lastReadErrorUID, new DateTimeType());
751 synchronized (this) {
753 states.forEach((uid, state) -> {
754 tryUpdateState(uid, state);
757 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
758 String.format("Error (%s) with read. Request: %s. Description: %s. Message: %s",
759 error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
763 private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) {
764 if (hasConfigurationError()) {
766 } else if (!isWriteEnabled) {
769 if (error instanceof ModbusConnectionException) {
770 logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
771 error.getClass().getSimpleName(), error.toString());
772 } else if (error instanceof ModbusTransportException) {
773 logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
774 error.getClass().getSimpleName(), error.toString());
777 "Thing {} '{}' had {} error on write: {} (message: {}). Stack trace follows since this is unexpected error.",
778 getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
779 error.getMessage(), error);
781 Map<ChannelUID, State> states = new HashMap<>();
782 ChannelUID lastWriteErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_ERROR);
783 if (isLinked(lastWriteErrorUID)) {
784 states.put(lastWriteErrorUID, new DateTimeType());
787 synchronized (this) {
789 states.forEach((uid, state) -> {
790 tryUpdateState(uid, state);
793 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
794 String.format("Error (%s) with write. Request: %s. Description: %s. Message: %s",
795 error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
799 public synchronized void onWriteResponse(AsyncModbusWriteResult result) {
800 if (hasConfigurationError()) {
802 } else if (!isWriteEnabled) {
805 logger.debug("Successful write, matching request {}", result.getRequest());
806 updateStatusIfChanged(ThingStatus.ONLINE);
807 ChannelUID lastWriteSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_SUCCESS);
808 if (isLinked(lastWriteSuccessUID)) {
809 updateState(lastWriteSuccessUID, new DateTimeType());
814 * Update linked channels
816 * @param numericState numeric state corresponding to polled data (or UNDEF with floating point NaN or infinity)
817 * @param boolValue boolean value corresponding to polled data
818 * @return updated channel data
820 private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) {
821 Transformation localReadTransformation = readTransformation;
822 if (localReadTransformation == null) {
823 // We should always have transformation available if thing is initalized properly
824 logger.trace("No transformation available, aborting processUpdatedValue");
825 return Collections.emptyMap();
827 Map<ChannelUID, State> states = new HashMap<>();
828 CHANNEL_ID_TO_ACCEPTED_TYPES.keySet().stream().forEach(channelId -> {
829 ChannelUID channelUID = getChannelUID(channelId);
830 if (!isLinked(channelUID)) {
833 List<Class<? extends State>> acceptedDataTypes = CHANNEL_ID_TO_ACCEPTED_TYPES.get(channelId);
834 if (acceptedDataTypes.isEmpty()) {
839 if (containsOnOff(acceptedDataTypes)) {
840 boolLikeState = boolValue ? OnOffType.ON : OnOffType.OFF;
841 } else if (containsOpenClosed(acceptedDataTypes)) {
842 boolLikeState = boolValue ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
844 boolLikeState = null;
847 State transformedState;
848 if (localReadTransformation.isIdentityTransform()) {
849 if (boolLikeState != null) {
850 // A bit of smartness for ON/OFF and OPEN/CLOSED with boolean like items
851 transformedState = boolLikeState;
853 // Numeric states always go through transformation. This allows value of 17.5 to be
855 // 17.5% with percent types (instead of raising error)
856 transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
860 transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
864 if (transformedState != null) {
866 "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
867 channelId, transformedState, transformedState.getClass().getSimpleName(), numericState,
868 readValueType, boolValue,
869 localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
870 states.put(channelUID, transformedState);
872 String types = String.join(", ",
873 acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(String[]::new));
875 "Channel {} will not be updated since transformation was unsuccessful. Channel is expecting the following data types [{}]. Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
876 channelId, types, numericState, readValueType, boolValue,
877 localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
881 ChannelUID lastReadSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_SUCCESS);
882 if (isLinked(lastReadSuccessUID)) {
883 states.put(lastReadSuccessUID, new DateTimeType());
885 updateExpiredChannels(states);
889 private void updateExpiredChannels(Map<ChannelUID, State> states) {
890 synchronized (this) {
891 updateStatusIfChanged(ThingStatus.ONLINE);
892 long now = System.currentTimeMillis();
893 // Update channels that have not been updated in a while, or when their values has changed
894 states.forEach((uid, state) -> updateExpiredChannel(now, uid, state));
895 channelLastState = states;
899 // since lastState can be null, and "lastState == null" in conditional is not useless
900 @SuppressWarnings("null")
901 private void updateExpiredChannel(long now, ChannelUID uid, State state) {
903 State lastState = channelLastState.get(uid);
904 long lastUpdatedMillis = channelLastUpdated.getOrDefault(uid, 0L);
905 long millisSinceLastUpdate = now - lastUpdatedMillis;
906 if (lastUpdatedMillis <= 0L || lastState == null || updateUnchangedValuesEveryMillis <= 0L
907 || millisSinceLastUpdate > updateUnchangedValuesEveryMillis || !lastState.equals(state)) {
908 tryUpdateState(uid, state);
909 channelLastUpdated.put(uid, now);
913 private void tryUpdateState(ChannelUID uid, State state) {
915 updateState(uid, state);
916 } catch (IllegalArgumentException e) {
917 logger.warn("Error updating state '{}' (type {}) to channel {}: {} {}", state,
918 Optional.ofNullable(state).map(s -> s.getClass().getName()).orElse("null"), uid,
919 e.getClass().getName(), e.getMessage());
923 private ChannelUID getChannelUID(String channelID) {
925 .requireNonNull(channelCache.computeIfAbsent(channelID, id -> new ChannelUID(getThing().getUID(), id)));
928 private void updateStatusIfChanged(ThingStatus status) {
929 updateStatusIfChanged(status, ThingStatusDetail.NONE, null);
932 private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail,
933 @Nullable String description) {
934 ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, description);
935 Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
936 boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
937 if (statusInfo.getStatus() == ThingStatus.UNKNOWN || !statusInfo.equals(newStatusInfo) || intervalElapsed) {
938 statusInfo = newStatusInfo;
939 lastStatusInfoUpdate = LocalDateTime.now();
940 updateStatus(newStatusInfo);
945 * Update status using pre-constructed ThingStatusInfo
947 * Implementation adapted from BaseThingHandler updateStatus implementations
949 * @param statusInfo new status info
951 protected void updateStatus(ThingStatusInfo statusInfo) {
952 synchronized (this) {
953 ThingHandlerCallback callback = getCallback();
954 if (callback != null) {
955 callback.statusUpdated(this.thing, statusInfo);
957 logger.warn("Handler {} tried updating the thing status although the handler was already disposed.",
958 this.getClass().getSimpleName());