]> git.basschouten.com Git - openhab-addons.git/blob
75e6cc9de8dac57d36a5c3f06f4c52f42867f697
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.internal.handler;
14
15 import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
16
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;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Optional;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.atomic.AtomicReference;
29
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;
87
88 /**
89  * The {@link ModbusDataThingHandler} is responsible for interpreting polled modbus data, as well as handling openHAB
90  * commands
91  *
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
94  * synchronized
95  * to avoid data race conditions.
96  *
97  * @author Sami Salonen - Initial contribution
98  */
99 @NonNullByDefault
100 public class ModbusDataThingHandler extends BaseThingHandler {
101
102     private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class);
103
104     private final BundleContext bundleContext;
105
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<>();
108
109     static {
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());
124     }
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;
127
128     //
129     // If you change the below default/initial values, please update the corresponding values in dispose()
130     //
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);
154
155     private volatile LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
156     private volatile ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
157             null);
158
159     public ModbusDataThingHandler(Thing thing) {
160         super(thing);
161         this.bundleContext = FrameworkUtil.getBundle(ModbusDataThingHandler.class).getBundleContext();
162     }
163
164     @Override
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) {
170             return;
171         }
172
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
178                 return;
179             }
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
182             // onRegisters.
183             scheduler.schedule(() -> poller.refresh(), 0, TimeUnit.SECONDS);
184             return;
185         } else if (hasConfigurationError()) {
186             logger.debug(
187                     "Thing {} '{}' command '{}' to channel '{}': Thing has configuration error so ignoring the command",
188                     getThing().getUID(), getThing().getLabel(), command, channelUID);
189             return;
190         } else if (!isWriteEnabled) {
191             logger.debug(
192                     "Thing {} '{}' command '{}' to channel '{}': no writing configured -> aborting processing command",
193                     getThing().getUID(), getThing().getLabel(), command, channelUID);
194             return;
195         }
196
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
201             return;
202         }
203
204         // We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
205         // missing
206         Optional<Integer> writeStart = this.writeStart;
207         if (writeStart.isEmpty()) {
208             logger.debug(
209                     "Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
210                     getThing().getUID(), getThing().getLabel(), command);
211             return;
212         }
213
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);
218             return;
219         }
220
221         ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
222                 writeStart.get());
223         if (request == null) {
224             return;
225         }
226
227         logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint());
228         comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
229     }
230
231     /**
232      * Transform received command using the transformation.
233      *
234      * In case of JSON as transformation output, the output processed using {@link processJsonTransform}.
235      *
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
239      *
240      * @see processJsonTransform
241      */
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);
248         } else {
249             transformOutput = writeTransformation.transform(bundleContext, command.toString());
250             if (transformOutput.contains("[")) {
251                 processJsonTransform(command, transformOutput);
252                 return null;
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));
257                 return null;
258             } else {
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>"));
263             }
264         }
265         return transformedCommand;
266     }
267
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) {
275             // disposed thing
276             return null;
277         }
278         if (writeType.equals(WRITE_TYPE_COIL)) {
279             Optional<Boolean> commandAsBoolean = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
280             if (commandAsBoolean.isEmpty()) {
281                 logger.warn(
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);
284                 return null;
285             }
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");
295                 return null;
296             }
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");
302                     return null;
303                 }
304                 Optional<Boolean> commandBool = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
305                 if (commandBool.isEmpty()) {
306                     logger.warn(
307                             "Data thing is configured to write individual bit but we received command that is not convertible to 0/1 bit. Ignoring.");
308                     return null;
309                 } else if (pollerHandler == null) {
310                     logger.warn("Bug: sub index present but not child of poller. Should be in configuration erro");
311                     return null;
312                 }
313
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(),
320                                         commandBool.get()));
321                 if (mutatedRegisters == null) {
322                     logger.warn(
323                             "Received command to thing with writeValueType=bit (pointing to individual bit of a holding register) but internal cache not yet populated. Ignoring command");
324                     return null;
325                 }
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]);
331
332             } else {
333                 data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
334             }
335             writeMultiple = writeMultiple || data.size() > 1;
336             request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
337                     config.getWriteMaxTries());
338         } else {
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));
345         }
346         return request;
347     }
348
349     /**
350      * Combine boolean-like command with registers. Updated registers are returned
351      *
352      * @return
353      */
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);
362         if (b) {
363             allBytes[byteIndex] |= 1 << indexWithinByte;
364         } else {
365             allBytes[byteIndex] &= ~(1 << indexWithinByte);
366         }
367         if (logger.isTraceEnabled()) {
368             logger.trace(
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));
372         }
373         return new ModbusRegisterArray(allBytes);
374     }
375
376     private void processJsonTransform(Command command, String transformOutput) {
377         ModbusCommunicationInterface localComms = this.comms;
378         if (localComms == null) {
379             return;
380         }
381         Collection<ModbusWriteRequestBlueprint> requests;
382         try {
383             requests = WriteRequestJsonUtilities.fromJson(slaveId, transformOutput);
384         } catch (IllegalArgumentException | IllegalStateException e) {
385             logger.warn(
386                     "Thing {} '{}' could handle transformation result '{}'. Original command {}. Error details follow",
387                     getThing().getUID(), getThing().getLabel(), transformOutput, command, e);
388             return;
389         }
390
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);
395         });
396     }
397
398     @Override
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.
402         try {
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");
411                 return;
412             }
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(),
417                         bridge.getLabel());
418                 throw new ModbusConfigurationException(errmsg);
419             }
420             if (bridgeHandler instanceof ModbusEndpointThingHandler endpointHandler) {
421                 slaveId = endpointHandler.getSlaveId();
422                 comms = endpointHandler.getCommunicationInterface();
423                 childOfEndpoint = true;
424                 functionCode = null;
425                 readRequest = null;
426             } else if (bridgeHandler instanceof ModbusPollerThingHandler localPollerHandler) {
427                 pollerHandler = localPollerHandler;
428                 ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest();
429                 if (localReadRequest == null) {
430                     logger.debug(
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()));
435                     return;
436                 }
437                 readRequest = localReadRequest;
438                 slaveId = localReadRequest.getUnitID();
439                 functionCode = localReadRequest.getFunctionCode();
440                 comms = localPollerHandler.getCommunicationInterface();
441                 pollStart = localReadRequest.getReference();
442                 childOfEndpoint = false;
443             } else {
444                 String errmsg = String.format("Thing %s is connected to an unsupported type of bridge.",
445                         getThing().getUID());
446                 throw new ModbusConfigurationException(errmsg);
447             }
448
449             validateAndParseReadParameters(localConfig);
450             validateAndParseWriteParameters(localConfig);
451             validateMustReadOrWrite();
452
453             updateStatusIfChanged(ThingStatus.ONLINE);
454         } catch (ModbusConfigurationException | EndpointNotInitializedException e) {
455             logger.debug("Thing {} '{}' initialization error: {}", getThing().getUID(), getThing().getLabel(),
456                     e.getMessage());
457             updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
458         } finally {
459             logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
460         }
461     }
462
463     @Override
464     public synchronized void dispose() {
465         config = null;
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();
474         pollStart = 0;
475         slaveId = 0;
476         comms = null;
477         functionCode = null;
478         readRequest = null;
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);
489     }
490
491     @Override
492     public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
493         logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
494         this.dispose();
495         this.initialize();
496     }
497
498     private boolean hasConfigurationError() {
499         ThingStatusInfo statusInfo = getThing().getStatusInfo();
500         return statusInfo.getStatus() == ThingStatus.OFFLINE
501                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
502     }
503
504     private void validateMustReadOrWrite() throws ModbusConfigurationException {
505         if (!isReadEnabled && !isWriteEnabled) {
506             throw new ModbusConfigurationException("Should try to read or write data!");
507         }
508     }
509
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();
516
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);
523             }
524         }
525
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;
539             } else {
540                 try {
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);
546                 }
547             }
548
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);
554             }
555         } else {
556             isReadEnabled = false;
557         }
558
559         if (isReadEnabled) {
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()));
564             }
565             String[] readParts = readStart.split("\\.", 2);
566             try {
567                 readIndex = Optional.of(Integer.parseInt(readParts[0]));
568                 if (readParts.length == 2) {
569                     readSubIndex = Optional.of(Integer.parseInt(readParts[1]));
570                 } else {
571                     readSubIndex = Optional.empty();
572                 }
573             } catch (IllegalArgumentException e) {
574                 String errmsg = String.format("Thing %s invalid readStart: %s", getThing().getUID(),
575                         config.getReadStart());
576                 throw new ModbusConfigurationException(errmsg);
577             }
578         }
579         readTransformation = new CascadedValueTransformationImpl(config.getReadTransform());
580         validateReadIndex();
581     }
582
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);
612             }
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;
619             } else {
620                 try {
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);
625                 }
626             }
627
628             try {
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);
635                     }
636                     String[] writeParts = localWriteStart.split("\\.", 2);
637                     try {
638                         writeStart = Optional.of(Integer.parseInt(writeParts[0]));
639                         if (writeParts.length == 2) {
640                             writeSubIndex = Optional.of(Integer.parseInt(writeParts[1]));
641                         } else {
642                             writeSubIndex = Optional.empty();
643                         }
644                     } catch (IllegalArgumentException e) {
645                         String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
646                                 config.getReadStart());
647                         throw new ModbusConfigurationException(errmsg);
648                     }
649                 }
650             } catch (IllegalArgumentException e) {
651                 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
652                         config.getWriteStart());
653                 throw new ModbusConfigurationException(errmsg);
654             }
655
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);
667             }
668
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);
676                 }
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);
684                 }
685             }
686             validateWriteIndex();
687         } else {
688             isWriteEnabled = false;
689         }
690     }
691
692     private void validateReadIndex() throws ModbusConfigurationException {
693         @Nullable
694         ModbusReadRequestBlueprint readRequest = this.readRequest;
695         ValueType readValueType = this.readValueType;
696         if (readIndex.isEmpty() || readRequest == null) {
697             return;
698         }
699         assert readValueType != null;
700         // bits represented by the value type, e.g. int32 -> 32
701         int valueTypeBitCount = readValueType.getBits();
702         int dataElementBits;
703         switch (readRequest.getFunctionCode()) {
704             case READ_INPUT_REGISTERS:
705             case READ_MULTIPLE_REGISTERS:
706                 dataElementBits = 16;
707                 break;
708             case READ_COILS:
709             case READ_INPUT_DISCRETES:
710                 dataElementBits = 1;
711                 break;
712             default:
713                 throw new IllegalStateException(readRequest.getFunctionCode().toString());
714         }
715
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);
720         }
721
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);
736         }
737
738         // Determine bit positions polled, both start and end inclusive
739         int pollStartBitIndex = readRequest.getReference() * dataElementBits;
740         int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits - 1;
741
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;
745
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,
750                     readIndex.get());
751             throw new ModbusConfigurationException(errmsg);
752         }
753     }
754
755     private void validateWriteIndex() throws ModbusConfigurationException {
756         @Nullable
757         ModbusReadRequestBlueprint readRequest = this.readRequest;
758         if (writeStart.isEmpty() || writeSubIndex.isEmpty()) {
759             //
760             // this validation is really about writeStart=X.Y validation
761             //
762             return;
763         } else if (readRequest == null) {
764             // should not happen, already validated
765             throw new ModbusConfigurationException("Must poll data with writeStart=X.Y");
766         }
767
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);
772         }
773
774         // Determine bit positions polled, both start and end inclusive
775         int pollStartBitIndex = readRequest.getReference() * 16;
776         int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * 16 - 1;
777
778         // Determine bit positions read, both start and end inclusive
779         int writeStartBitIndex = writeStart.get() * 16 + readSubIndex.orElse(0);
780         int writeEndBitIndex = writeStartBitIndex - 1;
781
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);
787         }
788     }
789
790     private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
791         return channelAcceptedDataTypes.stream().anyMatch(clz -> clz.equals(OnOffType.class));
792     }
793
794     private boolean containsOpenClosed(List<Class<? extends State>> acceptedDataTypes) {
795         return acceptedDataTypes.stream().anyMatch(clz -> clz.equals(OpenClosedType.class));
796     }
797
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));
801     }
802
803     public synchronized void handleReadError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
804         onError(failure.getRequest(), failure.getCause());
805     }
806
807     public synchronized void handleWriteError(AsyncModbusFailure<ModbusWriteRequestBlueprint> failure) {
808         onError(failure.getRequest(), failure.getCause());
809     }
810
811     private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
812         if (hasConfigurationError()) {
813             return;
814         } else if (!isReadEnabled) {
815             return;
816         }
817         ValueType readValueType = this.readValueType;
818         if (readValueType == null) {
819             return;
820         }
821         State numericState;
822
823         // extractIndex:
824         // e.g. with bit, extractIndex=4 means 5th bit (from right) ("10.4" -> 5th bit of register 10, "10.4" -> 5th bit
825         // of register 10)
826         // bit of second register)
827         // e.g. with 8bit integer, extractIndex=3 means high byte of second register
828         //
829         // with <16 bit types, this is the index of the N'th 1-bit/8-bit item. Each register has 16/2 items,
830         // respectively.
831         // with >=16 bit types, this is index of first register
832         int extractIndex;
833         if (readValueType.getBits() >= 16) {
834             // Invariant, checked in initialize
835             assert readSubIndex.orElse(0) == 0;
836             extractIndex = readIndex.get() - pollStart;
837         } else {
838             int subIndex = readSubIndex.orElse(0);
839             int itemsPerRegister = 16 / readValueType.getBits();
840             extractIndex = (readIndex.get() - pollStart) * itemsPerRegister + subIndex;
841         }
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);
846         logger.debug(
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);
850     }
851
852     private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
853         if (hasConfigurationError()) {
854             return;
855         } else if (!isReadEnabled) {
856             return;
857         }
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);
861         logger.debug(
862                 "Thing {} channels updated: {}. readValueType={}, readIndex={} -> numeric value {} and boolValue={}. Bits {} for request {}",
863                 thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request);
864     }
865
866     private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) {
867         if (hasConfigurationError()) {
868             return;
869         } else if (!isReadEnabled) {
870             return;
871         }
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());
878         } else {
879             logger.error(
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);
883         }
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());
888         }
889
890         synchronized (this) {
891             // Update channels
892             states.forEach((uid, state) -> {
893                 tryUpdateState(uid, state);
894             });
895
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()));
899         }
900     }
901
902     private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) {
903         if (hasConfigurationError()) {
904             return;
905         } else if (!isWriteEnabled) {
906             return;
907         }
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());
914         } else {
915             logger.error(
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);
919         }
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());
924         }
925
926         synchronized (this) {
927             // Update channels
928             states.forEach((uid, state) -> {
929                 tryUpdateState(uid, state);
930             });
931
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()));
935         }
936     }
937
938     public synchronized void onWriteResponse(AsyncModbusWriteResult result) {
939         if (hasConfigurationError()) {
940             return;
941         } else if (!isWriteEnabled) {
942             return;
943         }
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());
949         }
950     }
951
952     /**
953      * Update linked channels
954      *
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
958      */
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();
965         }
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)) {
970                 return;
971             }
972             List<Class<? extends State>> acceptedDataTypes = CHANNEL_ID_TO_ACCEPTED_TYPES.get(channelId);
973             if (acceptedDataTypes.isEmpty()) {
974                 return;
975             }
976
977             State boolLikeState;
978             if (containsOnOff(acceptedDataTypes)) {
979                 boolLikeState = OnOffType.from(boolValue);
980             } else if (containsOpenClosed(acceptedDataTypes)) {
981                 boolLikeState = boolValue ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
982             } else {
983                 boolLikeState = null;
984             }
985
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;
991                 } else {
992                     // Numeric states always go through transformation. This allows value of 17.5 to be
993                     // converted to
994                     // 17.5% with percent types (instead of raising error)
995                     transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
996                             numericState);
997                 }
998             } else {
999                 transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
1000                         numericState);
1001             }
1002
1003             if (transformedState != null) {
1004                 logger.trace(
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);
1010             } else {
1011                 String types = String.join(", ",
1012                         acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(String[]::new));
1013                 logger.warn(
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);
1017             }
1018         });
1019
1020         ChannelUID lastReadSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_SUCCESS);
1021         if (isLinked(lastReadSuccessUID)) {
1022             states.put(lastReadSuccessUID, new DateTimeType());
1023         }
1024         updateExpiredChannels(states);
1025         return states;
1026     }
1027
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;
1035         }
1036     }
1037
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) {
1041         @Nullable
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);
1049         }
1050     }
1051
1052     private void tryUpdateState(ChannelUID uid, State state) {
1053         try {
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());
1059         }
1060     }
1061
1062     private ChannelUID getChannelUID(String channelID) {
1063         return Objects
1064                 .requireNonNull(channelCache.computeIfAbsent(channelID, id -> new ChannelUID(getThing().getUID(), id)));
1065     }
1066
1067     private void updateStatusIfChanged(ThingStatus status) {
1068         updateStatusIfChanged(status, ThingStatusDetail.NONE, null);
1069     }
1070
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);
1080         }
1081     }
1082
1083     /**
1084      * Update status using pre-constructed ThingStatusInfo
1085      *
1086      * Implementation adapted from BaseThingHandler updateStatus implementations
1087      *
1088      * @param statusInfo new status info
1089      */
1090     protected void updateStatus(ThingStatusInfo statusInfo) {
1091         synchronized (this) {
1092             ThingHandlerCallback callback = getCallback();
1093             if (callback != null) {
1094                 callback.statusUpdated(this.thing, statusInfo);
1095             } else {
1096                 logger.warn("Handler {} tried updating the thing status although the handler was already disposed.",
1097                         this.getClass().getSimpleName());
1098             }
1099         }
1100     }
1101 }