2 * Copyright (c) 2010-2024 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;
28 import java.util.concurrent.atomic.AtomicReference;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
33 import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
34 import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
35 import org.openhab.binding.modbus.internal.CascadedValueTransformationImpl;
36 import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
37 import org.openhab.binding.modbus.internal.ModbusConfigurationException;
38 import org.openhab.binding.modbus.internal.SingleValueTransformation;
39 import org.openhab.binding.modbus.internal.ValueTransformation;
40 import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration;
41 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
42 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
43 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
44 import org.openhab.core.io.transport.modbus.BitArray;
45 import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
46 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
47 import org.openhab.core.io.transport.modbus.ModbusConstants;
48 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
49 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
50 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
51 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
52 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
53 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
54 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
55 import org.openhab.core.io.transport.modbus.exception.ModbusConnectionException;
56 import org.openhab.core.io.transport.modbus.exception.ModbusTransportException;
57 import org.openhab.core.io.transport.modbus.json.WriteRequestJsonUtilities;
58 import org.openhab.core.library.items.ContactItem;
59 import org.openhab.core.library.items.DateTimeItem;
60 import org.openhab.core.library.items.DimmerItem;
61 import org.openhab.core.library.items.NumberItem;
62 import org.openhab.core.library.items.RollershutterItem;
63 import org.openhab.core.library.items.StringItem;
64 import org.openhab.core.library.items.SwitchItem;
65 import org.openhab.core.library.types.DateTimeType;
66 import org.openhab.core.library.types.DecimalType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.OpenClosedType;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingStatusInfo;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.BridgeHandler;
77 import org.openhab.core.thing.binding.ThingHandlerCallback;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.openhab.core.types.UnDefType;
82 import org.openhab.core.util.HexUtils;
83 import org.osgi.framework.BundleContext;
84 import org.osgi.framework.FrameworkUtil;
85 import org.slf4j.Logger;
86 import org.slf4j.LoggerFactory;
89 * The {@link ModbusDataThingHandler} is responsible for interpreting polled modbus data, as well as handling openHAB
92 * Thing can be re-initialized by the bridge in case of configuration changes (bridgeStatusChanged).
93 * Because of this, initialize, dispose and all callback methods (onRegisters, onBits, onError, onWriteResponse) are
95 * to avoid data race conditions.
97 * @author Sami Salonen - Initial contribution
100 public class ModbusDataThingHandler extends BaseThingHandler {
102 private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class);
104 private final BundleContext bundleContext;
106 private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
107 private static final Map<String, List<Class<? extends State>>> CHANNEL_ID_TO_ACCEPTED_TYPES = new HashMap<>();
110 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_SWITCH,
111 new SwitchItem("").getAcceptedDataTypes());
112 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_CONTACT,
113 new ContactItem("").getAcceptedDataTypes());
114 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DATETIME,
115 new DateTimeItem("").getAcceptedDataTypes());
116 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DIMMER,
117 new DimmerItem("").getAcceptedDataTypes());
118 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_NUMBER,
119 new NumberItem("").getAcceptedDataTypes());
120 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_STRING,
121 new StringItem("").getAcceptedDataTypes());
122 CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_ROLLERSHUTTER,
123 new RollershutterItem("").getAcceptedDataTypes());
125 // data channels + 4 for read/write last error/success
126 private static final int NUMER_OF_CHANNELS_HINT = CHANNEL_ID_TO_ACCEPTED_TYPES.size() + 4;
129 // If you change the below default/initial values, please update the corresponding values in dispose()
131 private volatile @Nullable ModbusDataConfiguration config;
132 private volatile @Nullable ValueType readValueType;
133 private volatile @Nullable ValueType writeValueType;
134 private volatile @Nullable CascadedValueTransformationImpl readTransformation;
135 private volatile @Nullable CascadedValueTransformationImpl writeTransformation;
136 private volatile Optional<Integer> readIndex = Optional.empty();
137 private volatile Optional<Integer> readSubIndex = Optional.empty();
138 private volatile Optional<Integer> writeStart = Optional.empty();
139 private volatile Optional<Integer> writeSubIndex = Optional.empty();
140 private volatile int pollStart;
141 private volatile int slaveId;
142 private volatile @Nullable ModbusReadFunctionCode functionCode;
143 private volatile @Nullable ModbusReadRequestBlueprint readRequest;
144 private volatile long updateUnchangedValuesEveryMillis;
145 private volatile @NonNullByDefault({}) ModbusCommunicationInterface comms;
146 private volatile boolean isWriteEnabled;
147 private volatile boolean isReadEnabled;
148 private volatile boolean writeParametersHavingTransformationOnly;
149 private volatile boolean childOfEndpoint;
150 private volatile @Nullable ModbusPollerThingHandler pollerHandler;
151 private volatile Map<String, ChannelUID> channelCache = new HashMap<>();
152 private volatile Map<ChannelUID, Long> channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
153 private volatile Map<ChannelUID, State> channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
155 private volatile LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
156 private volatile ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
159 public ModbusDataThingHandler(Thing thing) {
161 this.bundleContext = FrameworkUtil.getBundle(ModbusDataThingHandler.class).getBundleContext();
165 public synchronized void handleCommand(ChannelUID channelUID, Command command) {
166 logger.trace("Thing {} '{}' received command '{}' to channel '{}'", getThing().getUID(), getThing().getLabel(),
167 command, channelUID);
168 ModbusDataConfiguration config = this.config;
169 if (config == null) {
173 if (RefreshType.REFRESH == command) {
174 ModbusPollerThingHandler poller = pollerHandler;
175 if (poller == null) {
176 // Data thing must be child of endpoint, and thus write-only.
177 // There is no data to update
180 // We *schedule* the REFRESH to avoid dead-lock situation where poller is trying update this
181 // data thing with cached data (resulting in deadlock in two synchronized methods: this (handleCommand) and
183 scheduler.schedule(() -> poller.refresh(), 0, TimeUnit.SECONDS);
185 } else if (hasConfigurationError()) {
187 "Thing {} '{}' command '{}' to channel '{}': Thing has configuration error so ignoring the command",
188 getThing().getUID(), getThing().getLabel(), command, channelUID);
190 } else if (!isWriteEnabled) {
192 "Thing {} '{}' command '{}' to channel '{}': no writing configured -> aborting processing command",
193 getThing().getUID(), getThing().getLabel(), command, channelUID);
197 Optional<Command> transformedCommand = transformCommandAndProcessJSON(channelUID, command);
198 if (transformedCommand == null) {
199 // We have, JSON as transform output (which has been processed) or some error. See
200 // transformCommandAndProcessJSON javadoc
204 // We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
206 Optional<Integer> writeStart = this.writeStart;
207 if (writeStart.isEmpty()) {
209 "Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
210 getThing().getUID(), getThing().getLabel(), command);
214 if (transformedCommand.isEmpty()) {
215 // transformation failed, return
216 logger.warn("Cannot process command {} (of type {}) with channel {} since transformation was unsuccessful",
217 command, command.getClass().getSimpleName(), channelUID);
221 ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
223 if (request == null) {
227 logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint());
228 comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
232 * Transform received command using the transformation.
234 * In case of JSON as transformation output, the output processed using {@link processJsonTransform}.
236 * @param channelUID channel UID corresponding to received command
237 * @param command command to be transformed
238 * @return transformed command. Null is returned with JSON transformation outputs and configuration errors
240 * @see processJsonTransform
242 private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
243 String transformOutput;
244 Optional<Command> transformedCommand;
245 ValueTransformation writeTransformation = this.writeTransformation;
246 if (writeTransformation == null || writeTransformation.isIdentityTransform()) {
247 transformedCommand = Optional.of(command);
249 transformOutput = writeTransformation.transform(bundleContext, command.toString());
250 if (transformOutput.contains("[")) {
251 processJsonTransform(command, transformOutput);
253 } else if (writeParametersHavingTransformationOnly) {
254 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
255 "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",
256 command, channelUID));
259 transformedCommand = SingleValueTransformation.tryConvertToCommand(transformOutput);
260 logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput,
261 transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"),
262 transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
265 return transformedCommand;
268 private @Nullable ModbusWriteRequestBlueprint requestFromCommand(ChannelUID channelUID, Command origCommand,
269 ModbusDataConfiguration config, Command transformedCommand, Integer writeStart) {
270 ModbusWriteRequestBlueprint request;
271 boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
272 String writeType = config.getWriteType();
273 ModbusPollerThingHandler pollerHandler = this.pollerHandler;
274 if (writeType == null) {
278 if (writeType.equals(WRITE_TYPE_COIL)) {
279 Optional<Boolean> commandAsBoolean = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
280 if (commandAsBoolean.isEmpty()) {
282 "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 '{}'",
283 origCommand, channelUID, transformedCommand);
286 boolean data = commandAsBoolean.get();
287 request = new ModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple,
288 config.getWriteMaxTries());
289 } else if (writeType.equals(WRITE_TYPE_HOLDING)) {
290 ValueType writeValueType = this.writeValueType;
291 if (writeValueType == null) {
292 // Should not happen in practice, since we are not in configuration error (checked above)
293 // This will make compiler happy anyways with the null checks
294 logger.warn("Received command but write value type not set! Ignoring command");
297 final ModbusRegisterArray data;
298 if (writeValueType.equals(ValueType.BIT)) {
299 if (writeSubIndex.isEmpty()) {
300 // Should not happen! should be in configuration error
301 logger.error("Bug: sub index not present but writeValueType=BIT. Should be in configuration error");
304 Optional<Boolean> commandBool = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
305 if (commandBool.isEmpty()) {
307 "Data thing is configured to write individual bit but we received command that is not convertible to 0/1 bit. Ignoring.");
309 } else if (pollerHandler == null) {
310 logger.warn("Bug: sub index present but not child of poller. Should be in configuration erro");
314 // writing bit of an individual register. Using cache from poller
315 AtomicReference<@Nullable ModbusRegisterArray> cachedRegistersRef = pollerHandler
316 .getLastPolledDataCache();
317 ModbusRegisterArray mutatedRegisters = cachedRegistersRef
318 .updateAndGet(cachedRegisters -> cachedRegisters == null ? null
319 : combineCommandWithRegisters(cachedRegisters, writeStart, writeSubIndex.get(),
321 if (mutatedRegisters == null) {
323 "Received command to thing with writeValueType=bit (pointing to individual bit of a holding register) but internal cache not yet populated. Ignoring command");
326 // extract register (first byte index = register index * 2)
327 byte[] allMutatedBytes = mutatedRegisters.getBytes();
328 int writeStartRelative = writeStart - pollStart;
329 data = new ModbusRegisterArray(allMutatedBytes[writeStartRelative * 2],
330 allMutatedBytes[writeStartRelative * 2 + 1]);
333 data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
335 writeMultiple = writeMultiple || data.size() > 1;
336 request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
337 config.getWriteMaxTries());
339 // Should not happen! This method is not called in case configuration errors and writeType is validated
340 // already in initialization (validateAndParseWriteParameters).
341 // We keep this here for future-proofing the code (new writeType values)
342 throw new IllegalStateException(String.format(
343 "writeType does not equal %s or %s and thus configuration is invalid. Should not end up this far with configuration error.",
344 WRITE_TYPE_COIL, WRITE_TYPE_HOLDING));
350 * Combine boolean-like command with registers. Updated registers are returned
354 private ModbusRegisterArray combineCommandWithRegisters(ModbusRegisterArray registers, int registerIndex,
355 int bitIndex, boolean b) {
356 byte[] allBytes = registers.getBytes();
357 int bitIndexWithinRegister = bitIndex % 16;
358 boolean hiByte = bitIndexWithinRegister >= 8;
359 int indexWithinByte = bitIndexWithinRegister % 8;
360 int registerIndexRelative = registerIndex - pollStart;
361 int byteIndex = 2 * registerIndexRelative + (hiByte ? 0 : 1);
363 allBytes[byteIndex] |= 1 << indexWithinByte;
365 allBytes[byteIndex] &= ~(1 << indexWithinByte);
367 if (logger.isTraceEnabled()) {
369 "Boolean-like command {} from item, combining command with internal register ({}) with registerIndex={} (relative {}), bitIndex={}, resulting register {}",
370 b, HexUtils.bytesToHex(registers.getBytes()), registerIndex, registerIndexRelative, bitIndex,
371 HexUtils.bytesToHex(allBytes));
373 return new ModbusRegisterArray(allBytes);
376 private void processJsonTransform(Command command, String transformOutput) {
377 ModbusCommunicationInterface localComms = this.comms;
378 if (localComms == null) {
381 Collection<ModbusWriteRequestBlueprint> requests;
383 requests = WriteRequestJsonUtilities.fromJson(slaveId, transformOutput);
384 } catch (IllegalArgumentException | IllegalStateException e) {
386 "Thing {} '{}' could handle transformation result '{}'. Original command {}. Error details follow",
387 getThing().getUID(), getThing().getLabel(), transformOutput, command, e);
391 requests.stream().forEach(request -> {
392 logger.trace("Submitting write request: {} to endpoint {} (based from transformation {})", request,
393 localComms.getEndpoint(), transformOutput);
394 localComms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
399 public synchronized void initialize() {
400 // Initialize the thing. If done set status to ONLINE to indicate proper working.
401 // Long running initialization should be done asynchronously in background.
403 logger.trace("initialize() of thing {} '{}' starting", thing.getUID(), thing.getLabel());
404 ModbusDataConfiguration localConfig = config = getConfigAs(ModbusDataConfiguration.class);
405 updateUnchangedValuesEveryMillis = localConfig.getUpdateUnchangedValuesEveryMillis();
406 Bridge bridge = getBridge();
407 if (bridge == null || !bridge.getStatus().equals(ThingStatus.ONLINE)) {
408 logger.debug("Thing {} '{}' has no bridge or it is not online", getThing().getUID(),
409 getThing().getLabel());
410 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No online bridge");
413 BridgeHandler bridgeHandler = bridge.getHandler();
414 if (bridgeHandler == null) {
415 logger.warn("Bridge {} '{}' has no handler.", bridge.getUID(), bridge.getLabel());
416 String errmsg = String.format("Bridge %s '%s' configuration incomplete or with errors", bridge.getUID(),
418 throw new ModbusConfigurationException(errmsg);
420 if (bridgeHandler instanceof ModbusEndpointThingHandler endpointHandler) {
421 slaveId = endpointHandler.getSlaveId();
422 comms = endpointHandler.getCommunicationInterface();
423 childOfEndpoint = true;
426 } else if (bridgeHandler instanceof ModbusPollerThingHandler localPollerHandler) {
427 pollerHandler = localPollerHandler;
428 ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest();
429 if (localReadRequest == null) {
431 "Poller {} '{}' has no read request -- configuration is changing or bridge having invalid configuration?",
432 bridge.getUID(), bridge.getLabel());
433 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
434 String.format("Poller %s '%s' has no poll task", bridge.getUID(), bridge.getLabel()));
437 readRequest = localReadRequest;
438 slaveId = localReadRequest.getUnitID();
439 functionCode = localReadRequest.getFunctionCode();
440 comms = localPollerHandler.getCommunicationInterface();
441 pollStart = localReadRequest.getReference();
442 childOfEndpoint = false;
444 String errmsg = String.format("Thing %s is connected to an unsupported type of bridge.",
445 getThing().getUID());
446 throw new ModbusConfigurationException(errmsg);
449 validateAndParseReadParameters(localConfig);
450 validateAndParseWriteParameters(localConfig);
451 validateMustReadOrWrite();
453 updateStatusIfChanged(ThingStatus.ONLINE);
454 } catch (ModbusConfigurationException | EndpointNotInitializedException e) {
455 logger.debug("Thing {} '{}' initialization error: {}", getThing().getUID(), getThing().getLabel(),
457 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
459 logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
464 public synchronized void dispose() {
466 readValueType = null;
467 writeValueType = null;
468 readTransformation = null;
469 writeTransformation = null;
470 readIndex = Optional.empty();
471 readSubIndex = Optional.empty();
472 writeStart = Optional.empty();
473 writeSubIndex = Optional.empty();
479 isWriteEnabled = false;
480 isReadEnabled = false;
481 writeParametersHavingTransformationOnly = false;
482 childOfEndpoint = false;
483 pollerHandler = null;
484 channelCache = new HashMap<>();
485 lastStatusInfoUpdate = LocalDateTime.MIN;
486 statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null);
487 channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
488 channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
492 public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
493 logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
498 private boolean hasConfigurationError() {
499 ThingStatusInfo statusInfo = getThing().getStatusInfo();
500 return statusInfo.getStatus() == ThingStatus.OFFLINE
501 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
504 private void validateMustReadOrWrite() throws ModbusConfigurationException {
505 if (!isReadEnabled && !isWriteEnabled) {
506 throw new ModbusConfigurationException("Should try to read or write data!");
510 private void validateAndParseReadParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
511 ModbusReadFunctionCode functionCode = this.functionCode;
512 boolean readingDiscreteOrCoil = functionCode == ModbusReadFunctionCode.READ_COILS
513 || functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES;
514 boolean readStartMissing = config.getReadStart() == null || config.getReadStart().isBlank();
515 boolean readValueTypeMissing = config.getReadValueType() == null || config.getReadValueType().isBlank();
517 if (childOfEndpoint && readRequest == null) {
518 if (!readStartMissing || !readValueTypeMissing) {
519 String errmsg = String.format(
520 "Thing %s was configured for reading (readStart and/or readValueType specified) but the parent is not a polling bridge. Consider using a bridge of type 'Regular Poll'.",
521 getThing().getUID());
522 throw new ModbusConfigurationException(errmsg);
526 // we assume readValueType=bit by default if it is missing
527 boolean allMissingOrAllPresent = (readStartMissing && readValueTypeMissing)
528 || (!readStartMissing && (!readValueTypeMissing || readingDiscreteOrCoil));
529 if (!allMissingOrAllPresent) {
530 String errmsg = String.format(
531 "Thing %s readStart=%s, and readValueType=%s should be all present or all missing!",
532 getThing().getUID(), config.getReadStart(), config.getReadValueType());
533 throw new ModbusConfigurationException(errmsg);
534 } else if (!readStartMissing) {
535 // all read values are present
536 isReadEnabled = true;
537 if (readingDiscreteOrCoil && readValueTypeMissing) {
538 readValueType = ModbusConstants.ValueType.BIT;
541 readValueType = ValueType.fromConfigValue(config.getReadValueType());
542 } catch (IllegalArgumentException e) {
543 String errmsg = String.format("Thing %s readValueType=%s is invalid!", getThing().getUID(),
544 config.getReadValueType());
545 throw new ModbusConfigurationException(errmsg);
549 if (readingDiscreteOrCoil && !ModbusConstants.ValueType.BIT.equals(readValueType)) {
550 String errmsg = String.format(
551 "Thing %s invalid readValueType: Only readValueType='%s' (or undefined) supported with coils or discrete inputs. Value type was: %s",
552 getThing().getUID(), ModbusConstants.ValueType.BIT, config.getReadValueType());
553 throw new ModbusConfigurationException(errmsg);
556 isReadEnabled = false;
560 String readStart = config.getReadStart();
561 if (readStart == null) {
562 throw new ModbusConfigurationException(
563 String.format("Thing %s invalid readStart: %s", getThing().getUID(), config.getReadStart()));
565 String[] readParts = readStart.split("\\.", 2);
567 readIndex = Optional.of(Integer.parseInt(readParts[0]));
568 if (readParts.length == 2) {
569 readSubIndex = Optional.of(Integer.parseInt(readParts[1]));
571 readSubIndex = Optional.empty();
573 } catch (IllegalArgumentException e) {
574 String errmsg = String.format("Thing %s invalid readStart: %s", getThing().getUID(),
575 config.getReadStart());
576 throw new ModbusConfigurationException(errmsg);
579 readTransformation = new CascadedValueTransformationImpl(config.getReadTransform());
583 private void validateAndParseWriteParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
584 boolean writeTypeMissing = config.getWriteType() == null || config.getWriteType().isBlank();
585 boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank();
586 boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().isBlank();
587 boolean writeTransformationMissing = config.getWriteTransform() == null || config.getWriteTransform().isBlank();
588 writeTransformation = new CascadedValueTransformationImpl(config.getWriteTransform());
589 boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType());
590 writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing
591 && !writeTransformationMissing);
592 boolean allMissingOrAllPresentOrOnlyNonDefaultTransform = //
593 // read-only thing, no write specified
594 (writeTypeMissing && writeStartMissing && writeValueTypeMissing)
595 // mandatory write parameters provided. With coils one can drop value type
596 || (!writeTypeMissing && !writeStartMissing && (!writeValueTypeMissing || writingCoil))
597 // only transformation provided
598 || writeParametersHavingTransformationOnly;
599 if (!allMissingOrAllPresentOrOnlyNonDefaultTransform) {
600 String errmsg = String.format(
601 "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.",
602 config.getWriteType(), config.getWriteStart(), config.getWriteValueType());
603 throw new ModbusConfigurationException(errmsg);
604 } else if (!writeTypeMissing || writeParametersHavingTransformationOnly) {
605 isWriteEnabled = true;
606 // all write values are present
607 if (!writeParametersHavingTransformationOnly && !WRITE_TYPE_HOLDING.equals(config.getWriteType())
608 && !WRITE_TYPE_COIL.equals(config.getWriteType())) {
609 String errmsg = String.format("Invalid writeType=%s. Expecting %s or %s!", config.getWriteType(),
610 WRITE_TYPE_HOLDING, WRITE_TYPE_COIL);
611 throw new ModbusConfigurationException(errmsg);
613 final ValueType localWriteValueType;
614 if (writeParametersHavingTransformationOnly) {
615 // Placeholder for further checks
616 localWriteValueType = writeValueType = ModbusConstants.ValueType.INT16;
617 } else if (writingCoil && writeValueTypeMissing) {
618 localWriteValueType = writeValueType = ModbusConstants.ValueType.BIT;
621 localWriteValueType = writeValueType = ValueType.fromConfigValue(config.getWriteValueType());
622 } catch (IllegalArgumentException e) {
623 String errmsg = String.format("Invalid writeValueType=%s!", config.getWriteValueType());
624 throw new ModbusConfigurationException(errmsg);
629 if (!writeParametersHavingTransformationOnly) {
630 String localWriteStart = config.getWriteStart();
631 if (localWriteStart == null) {
632 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
633 config.getWriteStart());
634 throw new ModbusConfigurationException(errmsg);
636 String[] writeParts = localWriteStart.split("\\.", 2);
638 writeStart = Optional.of(Integer.parseInt(writeParts[0]));
639 if (writeParts.length == 2) {
640 writeSubIndex = Optional.of(Integer.parseInt(writeParts[1]));
642 writeSubIndex = Optional.empty();
644 } catch (IllegalArgumentException e) {
645 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
646 config.getReadStart());
647 throw new ModbusConfigurationException(errmsg);
650 } catch (IllegalArgumentException e) {
651 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
652 config.getWriteStart());
653 throw new ModbusConfigurationException(errmsg);
656 if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
657 String errmsg = String.format(
658 "Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
659 ModbusConstants.ValueType.BIT, config.getWriteValueType());
660 throw new ModbusConfigurationException(errmsg);
661 } else if (writeSubIndex.isEmpty() && !writingCoil && localWriteValueType.getBits() < 16) {
662 // trying to write holding registers with < 16 bit value types. Not supported
663 String errmsg = String.format(
664 "Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
665 config.getWriteValueType());
666 throw new ModbusConfigurationException(errmsg);
669 if (writeSubIndex.isPresent()) {
670 if (writeValueTypeMissing || writeTypeMissing || !WRITE_TYPE_HOLDING.equals(config.getWriteType())
671 || !ModbusConstants.ValueType.BIT.equals(localWriteValueType) || childOfEndpoint) {
672 String errmsg = String.format(
673 "Thing %s invalid writeType, writeValueType or parent. Since writeStart=X.Y, one should set writeType=holding, writeValueType=bit and have the thing as child of poller",
674 getThing().getUID(), config.getWriteStart());
675 throw new ModbusConfigurationException(errmsg);
677 ModbusReadRequestBlueprint readRequest = this.readRequest;
678 if (readRequest == null
679 || readRequest.getFunctionCode() != ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
680 String errmsg = String.format(
681 "Thing %s invalid. Since writeStart=X.Y, expecting poller reading holding registers.",
682 getThing().getUID());
683 throw new ModbusConfigurationException(errmsg);
686 validateWriteIndex();
688 isWriteEnabled = false;
692 private void validateReadIndex() throws ModbusConfigurationException {
694 ModbusReadRequestBlueprint readRequest = this.readRequest;
695 ValueType readValueType = this.readValueType;
696 if (readIndex.isEmpty() || readRequest == null) {
699 assert readValueType != null;
700 // bits represented by the value type, e.g. int32 -> 32
701 int valueTypeBitCount = readValueType.getBits();
703 switch (readRequest.getFunctionCode()) {
704 case READ_INPUT_REGISTERS:
705 case READ_MULTIPLE_REGISTERS:
706 dataElementBits = 16;
709 case READ_INPUT_DISCRETES:
713 throw new IllegalStateException(readRequest.getFunctionCode().toString());
716 boolean bitQuery = dataElementBits == 1;
717 if (bitQuery && readSubIndex.isPresent()) {
718 String errmsg = String.format("readStart=X.Y is not allowed to be used with coils or discrete inputs!");
719 throw new ModbusConfigurationException(errmsg);
722 if (valueTypeBitCount >= 16 && readSubIndex.isPresent()) {
723 String errmsg = String.format(
724 "readStart=X.Y notation is not allowed to be used with value types larger than 16bit! Use readStart=X instead.");
725 throw new ModbusConfigurationException(errmsg);
726 } else if (!bitQuery && valueTypeBitCount < 16 && readSubIndex.isEmpty()) {
727 // User has specified value type which is less than register width (16 bits).
728 // readStart=X.Y notation must be used to define which data to extract from the 16 bit register.
729 String errmsg = String
730 .format("readStart=X.Y must be used with value types (readValueType) less than 16bit!");
731 throw new ModbusConfigurationException(errmsg);
732 } else if (readSubIndex.isPresent() && (readSubIndex.get() + 1) * valueTypeBitCount > 16) {
733 // the sub index Y (in X.Y) is above the register limits
734 String errmsg = String.format("readStart=X.Y, the value Y is too large");
735 throw new ModbusConfigurationException(errmsg);
738 // Determine bit positions polled, both start and end inclusive
739 int pollStartBitIndex = readRequest.getReference() * dataElementBits;
740 int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits - 1;
742 // Determine bit positions read, both start and end inclusive
743 int readStartBitIndex = readIndex.get() * dataElementBits + readSubIndex.orElse(0) * valueTypeBitCount;
744 int readEndBitIndex = readStartBitIndex + valueTypeBitCount - 1;
746 if (readStartBitIndex < pollStartBitIndex || readEndBitIndex > pollEndBitIndex) {
747 String errmsg = String.format(
748 "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.",
749 pollStartBitIndex / dataElementBits, pollEndBitIndex / dataElementBits, readValueType,
751 throw new ModbusConfigurationException(errmsg);
755 private void validateWriteIndex() throws ModbusConfigurationException {
757 ModbusReadRequestBlueprint readRequest = this.readRequest;
758 if (writeStart.isEmpty() || writeSubIndex.isEmpty()) {
760 // this validation is really about writeStart=X.Y validation
763 } else if (readRequest == null) {
764 // should not happen, already validated
765 throw new ModbusConfigurationException("Must poll data with writeStart=X.Y");
768 if (writeSubIndex.isPresent() && (writeSubIndex.get() + 1) > 16) {
769 // the sub index Y (in X.Y) is above the register limits
770 String errmsg = String.format("readStart=X.Y, the value Y is too large");
771 throw new ModbusConfigurationException(errmsg);
774 // Determine bit positions polled, both start and end inclusive
775 int pollStartBitIndex = readRequest.getReference() * 16;
776 int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * 16 - 1;
778 // Determine bit positions read, both start and end inclusive
779 int writeStartBitIndex = writeStart.get() * 16 + readSubIndex.orElse(0);
780 int writeEndBitIndex = writeStartBitIndex - 1;
782 if (writeStartBitIndex < pollStartBitIndex || writeEndBitIndex > pollEndBitIndex) {
783 String errmsg = String.format(
784 "Out-of-bounds: Poller is reading from index %d to %d (inclusive) but this thing configured to write starting from element %d. Must write within polled limits",
785 pollStartBitIndex / 16, pollEndBitIndex / 16, writeStart.get());
786 throw new ModbusConfigurationException(errmsg);
790 private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
791 return channelAcceptedDataTypes.stream().anyMatch(clz -> clz.equals(OnOffType.class));
794 private boolean containsOpenClosed(List<Class<? extends State>> acceptedDataTypes) {
795 return acceptedDataTypes.stream().anyMatch(clz -> clz.equals(OpenClosedType.class));
798 public synchronized void onReadResult(AsyncModbusReadResult result) {
799 result.getRegisters().ifPresent(registers -> onRegisters(result.getRequest(), registers));
800 result.getBits().ifPresent(bits -> onBits(result.getRequest(), bits));
803 public synchronized void handleReadError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
804 onError(failure.getRequest(), failure.getCause());
807 public synchronized void handleWriteError(AsyncModbusFailure<ModbusWriteRequestBlueprint> failure) {
808 onError(failure.getRequest(), failure.getCause());
811 private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
812 if (hasConfigurationError()) {
814 } else if (!isReadEnabled) {
817 ValueType readValueType = this.readValueType;
818 if (readValueType == null) {
824 // e.g. with bit, extractIndex=4 means 5th bit (from right) ("10.4" -> 5th bit of register 10, "10.4" -> 5th bit
826 // bit of second register)
827 // e.g. with 8bit integer, extractIndex=3 means high byte of second register
829 // with <16 bit types, this is the index of the N'th 1-bit/8-bit item. Each register has 16/2 items,
831 // with >=16 bit types, this is index of first register
833 if (readValueType.getBits() >= 16) {
834 // Invariant, checked in initialize
835 assert readSubIndex.orElse(0) == 0;
836 extractIndex = readIndex.get() - pollStart;
838 int subIndex = readSubIndex.orElse(0);
839 int itemsPerRegister = 16 / readValueType.getBits();
840 extractIndex = (readIndex.get() - pollStart) * itemsPerRegister + subIndex;
842 numericState = ModbusBitUtilities.extractStateFromRegisters(registers, extractIndex, readValueType)
843 .map(state -> (State) state).orElse(UnDefType.UNDEF);
844 boolean boolValue = !numericState.equals(DecimalType.ZERO);
845 Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
847 "Thing {} channels updated: {}. readValueType={}, readIndex={}, readSubIndex(or 0)={}, extractIndex={} -> numeric value {} and boolValue={}. Registers {} for request {}",
848 thing.getUID(), values, readValueType, readIndex, readSubIndex.orElse(0), extractIndex, numericState,
849 boolValue, registers, request);
852 private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
853 if (hasConfigurationError()) {
855 } else if (!isReadEnabled) {
858 boolean boolValue = bits.getBit(readIndex.get() - pollStart);
859 DecimalType numericState = boolValue ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO;
860 Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
862 "Thing {} channels updated: {}. readValueType={}, readIndex={} -> numeric value {} and boolValue={}. Bits {} for request {}",
863 thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request);
866 private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) {
867 if (hasConfigurationError()) {
869 } else if (!isReadEnabled) {
872 if (error instanceof ModbusConnectionException) {
873 logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
874 error.getClass().getSimpleName(), error.toString());
875 } else if (error instanceof ModbusTransportException) {
876 logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
877 error.getClass().getSimpleName(), error.toString());
880 "Thing {} '{}' had {} error on read: {} (message: {}). Stack trace follows since this is unexpected error.",
881 getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
882 error.getMessage(), error);
884 Map<ChannelUID, State> states = new HashMap<>();
885 ChannelUID lastReadErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_ERROR);
886 if (isLinked(lastReadErrorUID)) {
887 states.put(lastReadErrorUID, new DateTimeType());
890 synchronized (this) {
892 states.forEach((uid, state) -> {
893 tryUpdateState(uid, state);
896 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
897 String.format("Error (%s) with read. Request: %s. Description: %s. Message: %s",
898 error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
902 private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) {
903 if (hasConfigurationError()) {
905 } else if (!isWriteEnabled) {
908 if (error instanceof ModbusConnectionException) {
909 logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
910 error.getClass().getSimpleName(), error.toString());
911 } else if (error instanceof ModbusTransportException) {
912 logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
913 error.getClass().getSimpleName(), error.toString());
916 "Thing {} '{}' had {} error on write: {} (message: {}). Stack trace follows since this is unexpected error.",
917 getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
918 error.getMessage(), error);
920 Map<ChannelUID, State> states = new HashMap<>();
921 ChannelUID lastWriteErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_ERROR);
922 if (isLinked(lastWriteErrorUID)) {
923 states.put(lastWriteErrorUID, new DateTimeType());
926 synchronized (this) {
928 states.forEach((uid, state) -> {
929 tryUpdateState(uid, state);
932 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
933 String.format("Error (%s) with write. Request: %s. Description: %s. Message: %s",
934 error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
938 public synchronized void onWriteResponse(AsyncModbusWriteResult result) {
939 if (hasConfigurationError()) {
941 } else if (!isWriteEnabled) {
944 logger.debug("Successful write, matching request {}", result.getRequest());
945 updateStatusIfChanged(ThingStatus.ONLINE);
946 ChannelUID lastWriteSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_SUCCESS);
947 if (isLinked(lastWriteSuccessUID)) {
948 updateState(lastWriteSuccessUID, new DateTimeType());
953 * Update linked channels
955 * @param numericState numeric state corresponding to polled data (or UNDEF with floating point NaN or infinity)
956 * @param boolValue boolean value corresponding to polled data
957 * @return updated channel data
959 private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) {
960 ValueTransformation localReadTransformation = readTransformation;
961 if (localReadTransformation == null) {
962 // We should always have transformation available if thing is initalized properly
963 logger.trace("No transformation available, aborting processUpdatedValue");
964 return Collections.emptyMap();
966 Map<ChannelUID, State> states = new HashMap<>();
967 CHANNEL_ID_TO_ACCEPTED_TYPES.keySet().stream().forEach(channelId -> {
968 ChannelUID channelUID = getChannelUID(channelId);
969 if (!isLinked(channelUID)) {
972 List<Class<? extends State>> acceptedDataTypes = CHANNEL_ID_TO_ACCEPTED_TYPES.get(channelId);
973 if (acceptedDataTypes.isEmpty()) {
978 if (containsOnOff(acceptedDataTypes)) {
979 boolLikeState = OnOffType.from(boolValue);
980 } else if (containsOpenClosed(acceptedDataTypes)) {
981 boolLikeState = boolValue ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
983 boolLikeState = null;
986 State transformedState;
987 if (localReadTransformation.isIdentityTransform()) {
988 if (boolLikeState != null) {
989 // A bit of smartness for ON/OFF and OPEN/CLOSED with boolean like items
990 transformedState = boolLikeState;
992 // Numeric states always go through transformation. This allows value of 17.5 to be
994 // 17.5% with percent types (instead of raising error)
995 transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
999 transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
1003 if (transformedState != null) {
1005 "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
1006 channelId, transformedState, transformedState.getClass().getSimpleName(), numericState,
1007 readValueType, boolValue,
1008 localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
1009 states.put(channelUID, transformedState);
1011 String types = String.join(", ",
1012 acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(String[]::new));
1014 "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: {}",
1015 channelId, types, numericState, readValueType, boolValue,
1016 localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
1020 ChannelUID lastReadSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_SUCCESS);
1021 if (isLinked(lastReadSuccessUID)) {
1022 states.put(lastReadSuccessUID, new DateTimeType());
1024 updateExpiredChannels(states);
1028 private void updateExpiredChannels(Map<ChannelUID, State> states) {
1029 synchronized (this) {
1030 updateStatusIfChanged(ThingStatus.ONLINE);
1031 long now = System.currentTimeMillis();
1032 // Update channels that have not been updated in a while, or when their values has changed
1033 states.forEach((uid, state) -> updateExpiredChannel(now, uid, state));
1034 channelLastState = states;
1038 // since lastState can be null, and "lastState == null" in conditional is not useless
1039 @SuppressWarnings("null")
1040 private void updateExpiredChannel(long now, ChannelUID uid, State state) {
1042 State lastState = channelLastState.get(uid);
1043 long lastUpdatedMillis = channelLastUpdated.getOrDefault(uid, 0L);
1044 long millisSinceLastUpdate = now - lastUpdatedMillis;
1045 if (lastUpdatedMillis <= 0L || lastState == null || updateUnchangedValuesEveryMillis <= 0L
1046 || millisSinceLastUpdate > updateUnchangedValuesEveryMillis || !lastState.equals(state)) {
1047 tryUpdateState(uid, state);
1048 channelLastUpdated.put(uid, now);
1052 private void tryUpdateState(ChannelUID uid, State state) {
1054 updateState(uid, state);
1055 } catch (IllegalArgumentException e) {
1056 logger.warn("Error updating state '{}' (type {}) to channel {}: {} {}", state,
1057 Optional.ofNullable(state).map(s -> s.getClass().getName()).orElse("null"), uid,
1058 e.getClass().getName(), e.getMessage());
1062 private ChannelUID getChannelUID(String channelID) {
1064 .requireNonNull(channelCache.computeIfAbsent(channelID, id -> new ChannelUID(getThing().getUID(), id)));
1067 private void updateStatusIfChanged(ThingStatus status) {
1068 updateStatusIfChanged(status, ThingStatusDetail.NONE, null);
1071 private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail,
1072 @Nullable String description) {
1073 ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, description);
1074 Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
1075 boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
1076 if (statusInfo.getStatus() == ThingStatus.UNKNOWN || !statusInfo.equals(newStatusInfo) || intervalElapsed) {
1077 statusInfo = newStatusInfo;
1078 lastStatusInfoUpdate = LocalDateTime.now();
1079 updateStatus(newStatusInfo);
1084 * Update status using pre-constructed ThingStatusInfo
1086 * Implementation adapted from BaseThingHandler updateStatus implementations
1088 * @param statusInfo new status info
1090 protected void updateStatus(ThingStatusInfo statusInfo) {
1091 synchronized (this) {
1092 ThingHandlerCallback callback = getCallback();
1093 if (callback != null) {
1094 callback.statusUpdated(this.thing, statusInfo);
1096 logger.warn("Handler {} tried updating the thing status although the handler was already disposed.",
1097 this.getClass().getSimpleName());