]> git.basschouten.com Git - openhab-addons.git/blob
833d043f9fa22e5a777978ad85e650b61d1dd00d
[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.ModbusBindingConstantsInternal;
36 import org.openhab.binding.modbus.internal.ModbusConfigurationException;
37 import org.openhab.binding.modbus.internal.ModbusTransformation;
38 import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration;
39 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
40 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
41 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
42 import org.openhab.core.io.transport.modbus.BitArray;
43 import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
44 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
45 import org.openhab.core.io.transport.modbus.ModbusConstants;
46 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
47 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
48 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
49 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
50 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
51 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
52 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
53 import org.openhab.core.io.transport.modbus.exception.ModbusConnectionException;
54 import org.openhab.core.io.transport.modbus.exception.ModbusTransportException;
55 import org.openhab.core.io.transport.modbus.json.WriteRequestJsonUtilities;
56 import org.openhab.core.library.items.ContactItem;
57 import org.openhab.core.library.items.DateTimeItem;
58 import org.openhab.core.library.items.DimmerItem;
59 import org.openhab.core.library.items.NumberItem;
60 import org.openhab.core.library.items.RollershutterItem;
61 import org.openhab.core.library.items.StringItem;
62 import org.openhab.core.library.items.SwitchItem;
63 import org.openhab.core.library.types.DateTimeType;
64 import org.openhab.core.library.types.DecimalType;
65 import org.openhab.core.library.types.OnOffType;
66 import org.openhab.core.library.types.OpenClosedType;
67 import org.openhab.core.thing.Bridge;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingStatusInfo;
73 import org.openhab.core.thing.binding.BaseThingHandler;
74 import org.openhab.core.thing.binding.BridgeHandler;
75 import org.openhab.core.thing.binding.ThingHandlerCallback;
76 import org.openhab.core.types.Command;
77 import org.openhab.core.types.RefreshType;
78 import org.openhab.core.types.State;
79 import org.openhab.core.types.UnDefType;
80 import org.openhab.core.util.HexUtils;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83
84 /**
85  * The {@link ModbusDataThingHandler} is responsible for interpreting polled modbus data, as well as handling openHAB
86  * commands
87  *
88  * Thing can be re-initialized by the bridge in case of configuration changes (bridgeStatusChanged).
89  * Because of this, initialize, dispose and all callback methods (onRegisters, onBits, onError, onWriteResponse) are
90  * synchronized
91  * to avoid data race conditions.
92  *
93  * @author Sami Salonen - Initial contribution
94  */
95 @NonNullByDefault
96 public class ModbusDataThingHandler extends BaseThingHandler {
97
98     private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class);
99
100     private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
101     private static final Map<String, List<Class<? extends State>>> CHANNEL_ID_TO_ACCEPTED_TYPES = new HashMap<>();
102
103     static {
104         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_SWITCH,
105                 new SwitchItem("").getAcceptedDataTypes());
106         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_CONTACT,
107                 new ContactItem("").getAcceptedDataTypes());
108         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DATETIME,
109                 new DateTimeItem("").getAcceptedDataTypes());
110         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DIMMER,
111                 new DimmerItem("").getAcceptedDataTypes());
112         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_NUMBER,
113                 new NumberItem("").getAcceptedDataTypes());
114         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_STRING,
115                 new StringItem("").getAcceptedDataTypes());
116         CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_ROLLERSHUTTER,
117                 new RollershutterItem("").getAcceptedDataTypes());
118     }
119     // data channels + 4 for read/write last error/success
120     private static final int NUMER_OF_CHANNELS_HINT = CHANNEL_ID_TO_ACCEPTED_TYPES.size() + 4;
121
122     //
123     // If you change the below default/initial values, please update the corresponding values in dispose()
124     //
125     private volatile @Nullable ModbusDataConfiguration config;
126     private volatile @Nullable ValueType readValueType;
127     private volatile @Nullable ValueType writeValueType;
128     private volatile @Nullable ModbusTransformation readTransformation;
129     private volatile @Nullable ModbusTransformation writeTransformation;
130     private volatile Optional<Integer> readIndex = Optional.empty();
131     private volatile Optional<Integer> readSubIndex = Optional.empty();
132     private volatile Optional<Integer> writeStart = Optional.empty();
133     private volatile Optional<Integer> writeSubIndex = Optional.empty();
134     private volatile int pollStart;
135     private volatile int slaveId;
136     private volatile @Nullable ModbusReadFunctionCode functionCode;
137     private volatile @Nullable ModbusReadRequestBlueprint readRequest;
138     private volatile long updateUnchangedValuesEveryMillis;
139     private volatile @NonNullByDefault({}) ModbusCommunicationInterface comms;
140     private volatile boolean isWriteEnabled;
141     private volatile boolean isReadEnabled;
142     private volatile boolean writeParametersHavingTransformationOnly;
143     private volatile boolean childOfEndpoint;
144     private volatile @Nullable ModbusPollerThingHandler pollerHandler;
145     private volatile Map<String, ChannelUID> channelCache = new HashMap<>();
146     private volatile Map<ChannelUID, Long> channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
147     private volatile Map<ChannelUID, State> channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
148
149     private volatile LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
150     private volatile ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
151             null);
152
153     public ModbusDataThingHandler(Thing thing) {
154         super(thing);
155     }
156
157     @Override
158     public synchronized void handleCommand(ChannelUID channelUID, Command command) {
159         logger.trace("Thing {} '{}' received command '{}' to channel '{}'", getThing().getUID(), getThing().getLabel(),
160                 command, channelUID);
161         ModbusDataConfiguration config = this.config;
162         if (config == null) {
163             return;
164         }
165
166         if (RefreshType.REFRESH == command) {
167             ModbusPollerThingHandler poller = pollerHandler;
168             if (poller == null) {
169                 // Data thing must be child of endpoint, and thus write-only.
170                 // There is no data to update
171                 return;
172             }
173             // We *schedule* the REFRESH to avoid dead-lock situation where poller is trying update this
174             // data thing with cached data (resulting in deadlock in two synchronized methods: this (handleCommand) and
175             // onRegisters.
176             scheduler.schedule(() -> poller.refresh(), 0, TimeUnit.SECONDS);
177             return;
178         } else if (hasConfigurationError()) {
179             logger.debug(
180                     "Thing {} '{}' command '{}' to channel '{}': Thing has configuration error so ignoring the command",
181                     getThing().getUID(), getThing().getLabel(), command, channelUID);
182             return;
183         } else if (!isWriteEnabled) {
184             logger.debug(
185                     "Thing {} '{}' command '{}' to channel '{}': no writing configured -> aborting processing command",
186                     getThing().getUID(), getThing().getLabel(), command, channelUID);
187             return;
188         }
189
190         Optional<Command> transformedCommand = transformCommandAndProcessJSON(channelUID, command);
191         if (transformedCommand == null) {
192             // We have, JSON as transform output (which has been processed) or some error. See
193             // transformCommandAndProcessJSON javadoc
194             return;
195         }
196
197         // We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
198         // missing
199         Optional<Integer> writeStart = this.writeStart;
200         if (writeStart.isEmpty()) {
201             logger.debug(
202                     "Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
203                     getThing().getUID(), getThing().getLabel(), command);
204             return;
205         }
206
207         if (transformedCommand.isEmpty()) {
208             // transformation failed, return
209             logger.warn("Cannot process command {} (of type {}) with channel {} since transformation was unsuccessful",
210                     command, command.getClass().getSimpleName(), channelUID);
211             return;
212         }
213
214         ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
215                 writeStart.get());
216         if (request == null) {
217             return;
218         }
219
220         logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint());
221         comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
222     }
223
224     /**
225      * Transform received command using the transformation.
226      *
227      * In case of JSON as transformation output, the output processed using {@link processJsonTransform}.
228      *
229      * @param channelUID channel UID corresponding to received command
230      * @param command command to be transformed
231      * @return transformed command. Null is returned with JSON transformation outputs and configuration errors
232      *
233      * @see processJsonTransform
234      */
235     private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
236         String transformOutput;
237         Optional<Command> transformedCommand;
238         ModbusTransformation writeTransformation = this.writeTransformation;
239         if (writeTransformation == null || writeTransformation.isIdentityTransform()) {
240             transformedCommand = Optional.of(command);
241         } else {
242             transformOutput = writeTransformation.transform(command.toString());
243             if (transformOutput.contains("[")) {
244                 processJsonTransform(command, transformOutput);
245                 return null;
246             } else if (writeParametersHavingTransformationOnly) {
247                 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
248                         "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",
249                         command, channelUID));
250                 return null;
251             } else {
252                 transformedCommand = ModbusTransformation.tryConvertToCommand(transformOutput);
253                 if (logger.isTraceEnabled()) {
254                     logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput,
255                             transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"),
256                             transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
257                 }
258             }
259         }
260         return transformedCommand;
261     }
262
263     private @Nullable ModbusWriteRequestBlueprint requestFromCommand(ChannelUID channelUID, Command origCommand,
264             ModbusDataConfiguration config, Command transformedCommand, Integer writeStart) {
265         ModbusWriteRequestBlueprint request;
266         boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
267         String writeType = config.getWriteType();
268         ModbusPollerThingHandler pollerHandler = this.pollerHandler;
269         if (writeType == null) {
270             // disposed thing
271             return null;
272         }
273         if (writeType.equals(WRITE_TYPE_COIL)) {
274             Optional<Boolean> commandAsBoolean = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
275             if (commandAsBoolean.isEmpty()) {
276                 logger.warn(
277                         "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 '{}'",
278                         origCommand, channelUID, transformedCommand);
279                 return null;
280             }
281             boolean data = commandAsBoolean.get();
282             request = new ModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple,
283                     config.getWriteMaxTries());
284         } else if (writeType.equals(WRITE_TYPE_HOLDING)) {
285             ValueType writeValueType = this.writeValueType;
286             if (writeValueType == null) {
287                 // Should not happen in practice, since we are not in configuration error (checked above)
288                 // This will make compiler happy anyways with the null checks
289                 logger.warn("Received command but write value type not set! Ignoring command");
290                 return null;
291             }
292             final ModbusRegisterArray data;
293             if (writeValueType.equals(ValueType.BIT)) {
294                 if (writeSubIndex.isEmpty()) {
295                     // Should not happen! should be in configuration error
296                     logger.error("Bug: sub index not present but writeValueType=BIT. Should be in configuration error");
297                     return null;
298                 }
299                 Optional<Boolean> commandBool = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
300                 if (commandBool.isEmpty()) {
301                     logger.warn(
302                             "Data thing is configured to write individual bit but we received command that is not convertible to 0/1 bit. Ignoring.");
303                     return null;
304                 } else if (pollerHandler == null) {
305                     logger.warn("Bug: sub index present but not child of poller. Should be in configuration erro");
306                     return null;
307                 }
308
309                 // writing bit of an individual register. Using cache from poller
310                 AtomicReference<@Nullable ModbusRegisterArray> cachedRegistersRef = pollerHandler
311                         .getLastPolledDataCache();
312                 ModbusRegisterArray mutatedRegisters = cachedRegistersRef
313                         .updateAndGet(cachedRegisters -> cachedRegisters == null ? null
314                                 : combineCommandWithRegisters(cachedRegisters, writeStart, writeSubIndex.get(),
315                                         commandBool.get()));
316                 if (mutatedRegisters == null) {
317                     logger.warn(
318                             "Received command to thing with writeValueType=bit (pointing to individual bit of a holding register) but internal cache not yet populated. Ignoring command");
319                     return null;
320                 }
321                 // extract register (first byte index = register index * 2)
322                 byte[] allMutatedBytes = mutatedRegisters.getBytes();
323                 int writeStartRelative = writeStart - pollStart;
324                 data = new ModbusRegisterArray(allMutatedBytes[writeStartRelative * 2],
325                         allMutatedBytes[writeStartRelative * 2 + 1]);
326
327             } else {
328                 data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
329             }
330             writeMultiple = writeMultiple || data.size() > 1;
331             request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
332                     config.getWriteMaxTries());
333         } else {
334             // Should not happen! This method is not called in case configuration errors and writeType is validated
335             // already in initialization (validateAndParseWriteParameters).
336             // We keep this here for future-proofing the code (new writeType values)
337             throw new IllegalStateException(String.format(
338                     "writeType does not equal %s or %s and thus configuration is invalid. Should not end up this far with configuration error.",
339                     WRITE_TYPE_COIL, WRITE_TYPE_HOLDING));
340         }
341         return request;
342     }
343
344     /**
345      * Combine boolean-like command with registers. Updated registers are returned
346      *
347      * @return
348      */
349     private ModbusRegisterArray combineCommandWithRegisters(ModbusRegisterArray registers, int registerIndex,
350             int bitIndex, boolean b) {
351         byte[] allBytes = registers.getBytes();
352         int bitIndexWithinRegister = bitIndex % 16;
353         boolean hiByte = bitIndexWithinRegister >= 8;
354         int indexWithinByte = bitIndexWithinRegister % 8;
355         int registerIndexRelative = registerIndex - pollStart;
356         int byteIndex = 2 * registerIndexRelative + (hiByte ? 0 : 1);
357         if (b) {
358             allBytes[byteIndex] |= 1 << indexWithinByte;
359         } else {
360             allBytes[byteIndex] &= ~(1 << indexWithinByte);
361         }
362         if (logger.isTraceEnabled()) {
363             logger.trace(
364                     "Boolean-like command {} from item, combining command with internal register ({}) with registerIndex={} (relative {}), bitIndex={}, resulting register {}",
365                     b, HexUtils.bytesToHex(registers.getBytes()), registerIndex, registerIndexRelative, bitIndex,
366                     HexUtils.bytesToHex(allBytes));
367         }
368         return new ModbusRegisterArray(allBytes);
369     }
370
371     private void processJsonTransform(Command command, String transformOutput) {
372         ModbusCommunicationInterface localComms = this.comms;
373         if (localComms == null) {
374             return;
375         }
376         Collection<ModbusWriteRequestBlueprint> requests;
377         try {
378             requests = WriteRequestJsonUtilities.fromJson(slaveId, transformOutput);
379         } catch (IllegalArgumentException | IllegalStateException e) {
380             logger.warn(
381                     "Thing {} '{}' could handle transformation result '{}'. Original command {}. Error details follow",
382                     getThing().getUID(), getThing().getLabel(), transformOutput, command, e);
383             return;
384         }
385
386         requests.stream().forEach(request -> {
387             logger.trace("Submitting write request: {} to endpoint {} (based from transformation {})", request,
388                     localComms.getEndpoint(), transformOutput);
389             localComms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
390         });
391     }
392
393     @Override
394     public synchronized void initialize() {
395         // Initialize the thing. If done set status to ONLINE to indicate proper working.
396         // Long running initialization should be done asynchronously in background.
397         try {
398             logger.trace("initialize() of thing {} '{}' starting", thing.getUID(), thing.getLabel());
399             ModbusDataConfiguration localConfig = config = getConfigAs(ModbusDataConfiguration.class);
400             updateUnchangedValuesEveryMillis = localConfig.getUpdateUnchangedValuesEveryMillis();
401             Bridge bridge = getBridge();
402             if (bridge == null || !bridge.getStatus().equals(ThingStatus.ONLINE)) {
403                 logger.debug("Thing {} '{}' has no bridge or it is not online", getThing().getUID(),
404                         getThing().getLabel());
405                 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No online bridge");
406                 return;
407             }
408             BridgeHandler bridgeHandler = bridge.getHandler();
409             if (bridgeHandler == null) {
410                 logger.warn("Bridge {} '{}' has no handler.", bridge.getUID(), bridge.getLabel());
411                 String errmsg = String.format("Bridge %s '%s' configuration incomplete or with errors", bridge.getUID(),
412                         bridge.getLabel());
413                 throw new ModbusConfigurationException(errmsg);
414             }
415             if (bridgeHandler instanceof ModbusEndpointThingHandler endpointHandler) {
416                 slaveId = endpointHandler.getSlaveId();
417                 comms = endpointHandler.getCommunicationInterface();
418                 childOfEndpoint = true;
419                 functionCode = null;
420                 readRequest = null;
421             } else if (bridgeHandler instanceof ModbusPollerThingHandler localPollerHandler) {
422                 pollerHandler = localPollerHandler;
423                 ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest();
424                 if (localReadRequest == null) {
425                     logger.debug(
426                             "Poller {} '{}' has no read request -- configuration is changing or bridge having invalid configuration?",
427                             bridge.getUID(), bridge.getLabel());
428                     updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
429                             String.format("Poller %s '%s' has no poll task", bridge.getUID(), bridge.getLabel()));
430                     return;
431                 }
432                 readRequest = localReadRequest;
433                 slaveId = localReadRequest.getUnitID();
434                 functionCode = localReadRequest.getFunctionCode();
435                 comms = localPollerHandler.getCommunicationInterface();
436                 pollStart = localReadRequest.getReference();
437                 childOfEndpoint = false;
438             } else {
439                 String errmsg = String.format("Thing %s is connected to an unsupported type of bridge.",
440                         getThing().getUID());
441                 throw new ModbusConfigurationException(errmsg);
442             }
443
444             validateAndParseReadParameters(localConfig);
445             validateAndParseWriteParameters(localConfig);
446             validateMustReadOrWrite();
447
448             updateStatusIfChanged(ThingStatus.ONLINE);
449         } catch (ModbusConfigurationException | EndpointNotInitializedException e) {
450             logger.debug("Thing {} '{}' initialization error: {}", getThing().getUID(), getThing().getLabel(),
451                     e.getMessage());
452             updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
453         } finally {
454             logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
455         }
456     }
457
458     @Override
459     public synchronized void dispose() {
460         config = null;
461         readValueType = null;
462         writeValueType = null;
463         readTransformation = null;
464         writeTransformation = null;
465         readIndex = Optional.empty();
466         readSubIndex = Optional.empty();
467         writeStart = Optional.empty();
468         writeSubIndex = Optional.empty();
469         pollStart = 0;
470         slaveId = 0;
471         comms = null;
472         functionCode = null;
473         readRequest = null;
474         isWriteEnabled = false;
475         isReadEnabled = false;
476         writeParametersHavingTransformationOnly = false;
477         childOfEndpoint = false;
478         pollerHandler = null;
479         channelCache = new HashMap<>();
480         lastStatusInfoUpdate = LocalDateTime.MIN;
481         statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null);
482         channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
483         channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
484     }
485
486     @Override
487     public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
488         logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
489         this.dispose();
490         this.initialize();
491     }
492
493     private boolean hasConfigurationError() {
494         ThingStatusInfo statusInfo = getThing().getStatusInfo();
495         return statusInfo.getStatus() == ThingStatus.OFFLINE
496                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
497     }
498
499     private void validateMustReadOrWrite() throws ModbusConfigurationException {
500         if (!isReadEnabled && !isWriteEnabled) {
501             throw new ModbusConfigurationException("Should try to read or write data!");
502         }
503     }
504
505     private void validateAndParseReadParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
506         ModbusReadFunctionCode functionCode = this.functionCode;
507         boolean readingDiscreteOrCoil = functionCode == ModbusReadFunctionCode.READ_COILS
508                 || functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES;
509         boolean readStartMissing = config.getReadStart() == null || config.getReadStart().isBlank();
510         boolean readValueTypeMissing = config.getReadValueType() == null || config.getReadValueType().isBlank();
511
512         if (childOfEndpoint && readRequest == null) {
513             if (!readStartMissing || !readValueTypeMissing) {
514                 String errmsg = String.format(
515                         "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'.",
516                         getThing().getUID());
517                 throw new ModbusConfigurationException(errmsg);
518             }
519         }
520
521         // we assume readValueType=bit by default if it is missing
522         boolean allMissingOrAllPresent = (readStartMissing && readValueTypeMissing)
523                 || (!readStartMissing && (!readValueTypeMissing || readingDiscreteOrCoil));
524         if (!allMissingOrAllPresent) {
525             String errmsg = String.format(
526                     "Thing %s readStart=%s, and readValueType=%s should be all present or all missing!",
527                     getThing().getUID(), config.getReadStart(), config.getReadValueType());
528             throw new ModbusConfigurationException(errmsg);
529         } else if (!readStartMissing) {
530             // all read values are present
531             isReadEnabled = true;
532             if (readingDiscreteOrCoil && readValueTypeMissing) {
533                 readValueType = ModbusConstants.ValueType.BIT;
534             } else {
535                 try {
536                     readValueType = ValueType.fromConfigValue(config.getReadValueType());
537                 } catch (IllegalArgumentException e) {
538                     String errmsg = String.format("Thing %s readValueType=%s is invalid!", getThing().getUID(),
539                             config.getReadValueType());
540                     throw new ModbusConfigurationException(errmsg);
541                 }
542             }
543
544             if (readingDiscreteOrCoil && !ModbusConstants.ValueType.BIT.equals(readValueType)) {
545                 String errmsg = String.format(
546                         "Thing %s invalid readValueType: Only readValueType='%s' (or undefined) supported with coils or discrete inputs. Value type was: %s",
547                         getThing().getUID(), ModbusConstants.ValueType.BIT, config.getReadValueType());
548                 throw new ModbusConfigurationException(errmsg);
549             }
550         } else {
551             isReadEnabled = false;
552         }
553
554         if (isReadEnabled) {
555             String readStart = config.getReadStart();
556             if (readStart == null) {
557                 throw new ModbusConfigurationException(
558                         String.format("Thing %s invalid readStart: %s", getThing().getUID(), config.getReadStart()));
559             }
560             String[] readParts = readStart.split("\\.", 2);
561             try {
562                 readIndex = Optional.of(Integer.parseInt(readParts[0]));
563                 if (readParts.length == 2) {
564                     readSubIndex = Optional.of(Integer.parseInt(readParts[1]));
565                 } else {
566                     readSubIndex = Optional.empty();
567                 }
568             } catch (IllegalArgumentException e) {
569                 String errmsg = String.format("Thing %s invalid readStart: %s", getThing().getUID(),
570                         config.getReadStart());
571                 throw new ModbusConfigurationException(errmsg);
572             }
573         }
574         readTransformation = new ModbusTransformation(config.getReadTransform());
575         validateReadIndex();
576     }
577
578     private void validateAndParseWriteParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
579         boolean writeTypeMissing = config.getWriteType() == null || config.getWriteType().isBlank();
580         boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank();
581         boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().isBlank();
582         boolean writeTransformationMissing = config.getWriteTransform() == null
583                 || String.join("", Objects.requireNonNull(config.getWriteTransform())).isBlank();
584
585         writeTransformation = new ModbusTransformation(config.getWriteTransform());
586         boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType());
587         writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing
588                 && !writeTransformationMissing);
589         boolean allMissingOrAllPresentOrOnlyNonDefaultTransform = //
590                 // read-only thing, no write specified
591                 (writeTypeMissing && writeStartMissing && writeValueTypeMissing)
592                         // mandatory write parameters provided. With coils one can drop value type
593                         || (!writeTypeMissing && !writeStartMissing && (!writeValueTypeMissing || writingCoil))
594                         // only transformation provided
595                         || writeParametersHavingTransformationOnly;
596         if (!allMissingOrAllPresentOrOnlyNonDefaultTransform) {
597             String errmsg = String.format(
598                     "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.",
599                     config.getWriteType(), config.getWriteStart(), config.getWriteValueType());
600             throw new ModbusConfigurationException(errmsg);
601         } else if (!writeTypeMissing || writeParametersHavingTransformationOnly) {
602             isWriteEnabled = true;
603             // all write values are present
604             if (!writeParametersHavingTransformationOnly && !WRITE_TYPE_HOLDING.equals(config.getWriteType())
605                     && !WRITE_TYPE_COIL.equals(config.getWriteType())) {
606                 String errmsg = String.format("Invalid writeType=%s. Expecting %s or %s!", config.getWriteType(),
607                         WRITE_TYPE_HOLDING, WRITE_TYPE_COIL);
608                 throw new ModbusConfigurationException(errmsg);
609             }
610             final ValueType localWriteValueType;
611             if (writeParametersHavingTransformationOnly) {
612                 // Placeholder for further checks
613                 localWriteValueType = writeValueType = ModbusConstants.ValueType.INT16;
614             } else if (writingCoil && writeValueTypeMissing) {
615                 localWriteValueType = writeValueType = ModbusConstants.ValueType.BIT;
616             } else {
617                 try {
618                     localWriteValueType = writeValueType = ValueType.fromConfigValue(config.getWriteValueType());
619                 } catch (IllegalArgumentException e) {
620                     String errmsg = String.format("Invalid writeValueType=%s!", config.getWriteValueType());
621                     throw new ModbusConfigurationException(errmsg);
622                 }
623             }
624
625             try {
626                 if (!writeParametersHavingTransformationOnly) {
627                     String localWriteStart = config.getWriteStart();
628                     if (localWriteStart == null) {
629                         String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
630                                 config.getWriteStart());
631                         throw new ModbusConfigurationException(errmsg);
632                     }
633                     String[] writeParts = localWriteStart.split("\\.", 2);
634                     try {
635                         writeStart = Optional.of(Integer.parseInt(writeParts[0]));
636                         if (writeParts.length == 2) {
637                             writeSubIndex = Optional.of(Integer.parseInt(writeParts[1]));
638                         } else {
639                             writeSubIndex = Optional.empty();
640                         }
641                     } catch (IllegalArgumentException e) {
642                         String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
643                                 config.getReadStart());
644                         throw new ModbusConfigurationException(errmsg);
645                     }
646                 }
647             } catch (IllegalArgumentException e) {
648                 String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
649                         config.getWriteStart());
650                 throw new ModbusConfigurationException(errmsg);
651             }
652
653             if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
654                 String errmsg = String.format(
655                         "Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
656                         ModbusConstants.ValueType.BIT, config.getWriteValueType());
657                 throw new ModbusConfigurationException(errmsg);
658             } else if (writeSubIndex.isEmpty() && !writingCoil && localWriteValueType.getBits() < 16) {
659                 // trying to write holding registers with < 16 bit value types. Not supported
660                 String errmsg = String.format(
661                         "Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
662                         config.getWriteValueType());
663                 throw new ModbusConfigurationException(errmsg);
664             }
665
666             if (writeSubIndex.isPresent()) {
667                 if (writeValueTypeMissing || writeTypeMissing || !WRITE_TYPE_HOLDING.equals(config.getWriteType())
668                         || !ModbusConstants.ValueType.BIT.equals(localWriteValueType) || childOfEndpoint) {
669                     String errmsg = String.format(
670                             "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",
671                             getThing().getUID(), config.getWriteStart());
672                     throw new ModbusConfigurationException(errmsg);
673                 }
674                 ModbusReadRequestBlueprint readRequest = this.readRequest;
675                 if (readRequest == null
676                         || readRequest.getFunctionCode() != ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
677                     String errmsg = String.format(
678                             "Thing %s invalid. Since writeStart=X.Y, expecting poller reading holding registers.",
679                             getThing().getUID());
680                     throw new ModbusConfigurationException(errmsg);
681                 }
682             }
683             validateWriteIndex();
684         } else {
685             isWriteEnabled = false;
686         }
687     }
688
689     private void validateReadIndex() throws ModbusConfigurationException {
690         @Nullable
691         ModbusReadRequestBlueprint readRequest = this.readRequest;
692         ValueType readValueType = this.readValueType;
693         if (readIndex.isEmpty() || readRequest == null) {
694             return;
695         }
696         assert readValueType != null;
697         // bits represented by the value type, e.g. int32 -> 32
698         int valueTypeBitCount = readValueType.getBits();
699         int dataElementBits;
700         switch (readRequest.getFunctionCode()) {
701             case READ_INPUT_REGISTERS:
702             case READ_MULTIPLE_REGISTERS:
703                 dataElementBits = 16;
704                 break;
705             case READ_COILS:
706             case READ_INPUT_DISCRETES:
707                 dataElementBits = 1;
708                 break;
709             default:
710                 throw new IllegalStateException(readRequest.getFunctionCode().toString());
711         }
712
713         boolean bitQuery = dataElementBits == 1;
714         if (bitQuery && readSubIndex.isPresent()) {
715             String errmsg = String.format("readStart=X.Y is not allowed to be used with coils or discrete inputs!");
716             throw new ModbusConfigurationException(errmsg);
717         }
718
719         if (valueTypeBitCount >= 16 && readSubIndex.isPresent()) {
720             String errmsg = String.format(
721                     "readStart=X.Y notation is not allowed to be used with value types larger than 16bit! Use readStart=X instead.");
722             throw new ModbusConfigurationException(errmsg);
723         } else if (!bitQuery && valueTypeBitCount < 16 && readSubIndex.isEmpty()) {
724             // User has specified value type which is less than register width (16 bits).
725             // readStart=X.Y notation must be used to define which data to extract from the 16 bit register.
726             String errmsg = String
727                     .format("readStart=X.Y must be used with value types (readValueType) less than 16bit!");
728             throw new ModbusConfigurationException(errmsg);
729         } else if (readSubIndex.isPresent() && (readSubIndex.get() + 1) * valueTypeBitCount > 16) {
730             // the sub index Y (in X.Y) is above the register limits
731             String errmsg = String.format("readStart=X.Y, the value Y is too large");
732             throw new ModbusConfigurationException(errmsg);
733         }
734
735         // Determine bit positions polled, both start and end inclusive
736         int pollStartBitIndex = readRequest.getReference() * dataElementBits;
737         int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits - 1;
738
739         // Determine bit positions read, both start and end inclusive
740         int readStartBitIndex = readIndex.get() * dataElementBits + readSubIndex.orElse(0) * valueTypeBitCount;
741         int readEndBitIndex = readStartBitIndex + valueTypeBitCount - 1;
742
743         if (readStartBitIndex < pollStartBitIndex || readEndBitIndex > pollEndBitIndex) {
744             String errmsg = String.format(
745                     "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.",
746                     pollStartBitIndex / dataElementBits, pollEndBitIndex / dataElementBits, readValueType,
747                     readIndex.get());
748             throw new ModbusConfigurationException(errmsg);
749         }
750     }
751
752     private void validateWriteIndex() throws ModbusConfigurationException {
753         @Nullable
754         ModbusReadRequestBlueprint readRequest = this.readRequest;
755         if (writeStart.isEmpty() || writeSubIndex.isEmpty()) {
756             //
757             // this validation is really about writeStart=X.Y validation
758             //
759             return;
760         } else if (readRequest == null) {
761             // should not happen, already validated
762             throw new ModbusConfigurationException("Must poll data with writeStart=X.Y");
763         }
764
765         if (writeSubIndex.isPresent() && (writeSubIndex.get() + 1) > 16) {
766             // the sub index Y (in X.Y) is above the register limits
767             String errmsg = String.format("readStart=X.Y, the value Y is too large");
768             throw new ModbusConfigurationException(errmsg);
769         }
770
771         // Determine bit positions polled, both start and end inclusive
772         int pollStartBitIndex = readRequest.getReference() * 16;
773         int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * 16 - 1;
774
775         // Determine bit positions read, both start and end inclusive
776         int writeStartBitIndex = writeStart.get() * 16 + readSubIndex.orElse(0);
777         int writeEndBitIndex = writeStartBitIndex - 1;
778
779         if (writeStartBitIndex < pollStartBitIndex || writeEndBitIndex > pollEndBitIndex) {
780             String errmsg = String.format(
781                     "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",
782                     pollStartBitIndex / 16, pollEndBitIndex / 16, writeStart.get());
783             throw new ModbusConfigurationException(errmsg);
784         }
785     }
786
787     private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
788         return channelAcceptedDataTypes.stream().anyMatch(clz -> clz.equals(OnOffType.class));
789     }
790
791     private boolean containsOpenClosed(List<Class<? extends State>> acceptedDataTypes) {
792         return acceptedDataTypes.stream().anyMatch(clz -> clz.equals(OpenClosedType.class));
793     }
794
795     public synchronized void onReadResult(AsyncModbusReadResult result) {
796         result.getRegisters().ifPresent(registers -> onRegisters(result.getRequest(), registers));
797         result.getBits().ifPresent(bits -> onBits(result.getRequest(), bits));
798     }
799
800     public synchronized void handleReadError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
801         onError(failure.getRequest(), failure.getCause());
802     }
803
804     public synchronized void handleWriteError(AsyncModbusFailure<ModbusWriteRequestBlueprint> failure) {
805         onError(failure.getRequest(), failure.getCause());
806     }
807
808     private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
809         if (hasConfigurationError()) {
810             return;
811         } else if (!isReadEnabled) {
812             return;
813         }
814         ValueType readValueType = this.readValueType;
815         if (readValueType == null) {
816             return;
817         }
818         State numericState;
819
820         // extractIndex:
821         // e.g. with bit, extractIndex=4 means 5th bit (from right) ("10.4" -> 5th bit of register 10, "10.4" -> 5th bit
822         // of register 10)
823         // bit of second register)
824         // e.g. with 8bit integer, extractIndex=3 means high byte of second register
825         //
826         // with <16 bit types, this is the index of the N'th 1-bit/8-bit item. Each register has 16/2 items,
827         // respectively.
828         // with >=16 bit types, this is index of first register
829         int extractIndex;
830         if (readValueType.getBits() >= 16) {
831             // Invariant, checked in initialize
832             assert readSubIndex.orElse(0) == 0;
833             extractIndex = readIndex.get() - pollStart;
834         } else {
835             int subIndex = readSubIndex.orElse(0);
836             int itemsPerRegister = 16 / readValueType.getBits();
837             extractIndex = (readIndex.get() - pollStart) * itemsPerRegister + subIndex;
838         }
839         numericState = ModbusBitUtilities.extractStateFromRegisters(registers, extractIndex, readValueType)
840                 .map(state -> (State) state).orElse(UnDefType.UNDEF);
841         boolean boolValue = !numericState.equals(DecimalType.ZERO);
842         Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
843         logger.debug(
844                 "Thing {} channels updated: {}. readValueType={}, readIndex={}, readSubIndex(or 0)={}, extractIndex={} -> numeric value {} and boolValue={}. Registers {} for request {}",
845                 thing.getUID(), values, readValueType, readIndex, readSubIndex.orElse(0), extractIndex, numericState,
846                 boolValue, registers, request);
847     }
848
849     private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
850         if (hasConfigurationError()) {
851             return;
852         } else if (!isReadEnabled) {
853             return;
854         }
855         boolean boolValue = bits.getBit(readIndex.get() - pollStart);
856         DecimalType numericState = boolValue ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO;
857         Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
858         logger.debug(
859                 "Thing {} channels updated: {}. readValueType={}, readIndex={} -> numeric value {} and boolValue={}. Bits {} for request {}",
860                 thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request);
861     }
862
863     private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) {
864         if (hasConfigurationError()) {
865             return;
866         } else if (!isReadEnabled) {
867             return;
868         }
869         if (error instanceof ModbusConnectionException) {
870             logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
871                     error.getClass().getSimpleName(), error.toString());
872         } else if (error instanceof ModbusTransportException) {
873             logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
874                     error.getClass().getSimpleName(), error.toString());
875         } else {
876             logger.error(
877                     "Thing {} '{}' had {} error on read: {} (message: {}). Stack trace follows since this is unexpected error.",
878                     getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
879                     error.getMessage(), error);
880         }
881         Map<ChannelUID, State> states = new HashMap<>();
882         ChannelUID lastReadErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_ERROR);
883         if (isLinked(lastReadErrorUID)) {
884             states.put(lastReadErrorUID, new DateTimeType());
885         }
886
887         synchronized (this) {
888             // Update channels
889             states.forEach((uid, state) -> {
890                 tryUpdateState(uid, state);
891             });
892
893             updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
894                     String.format("Error (%s) with read. Request: %s. Description: %s. Message: %s",
895                             error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
896         }
897     }
898
899     private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) {
900         if (hasConfigurationError()) {
901             return;
902         } else if (!isWriteEnabled) {
903             return;
904         }
905         if (error instanceof ModbusConnectionException) {
906             logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
907                     error.getClass().getSimpleName(), error.toString());
908         } else if (error instanceof ModbusTransportException) {
909             logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
910                     error.getClass().getSimpleName(), error.toString());
911         } else {
912             logger.error(
913                     "Thing {} '{}' had {} error on write: {} (message: {}). Stack trace follows since this is unexpected error.",
914                     getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
915                     error.getMessage(), error);
916         }
917         Map<ChannelUID, State> states = new HashMap<>();
918         ChannelUID lastWriteErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_ERROR);
919         if (isLinked(lastWriteErrorUID)) {
920             states.put(lastWriteErrorUID, new DateTimeType());
921         }
922
923         synchronized (this) {
924             // Update channels
925             states.forEach((uid, state) -> {
926                 tryUpdateState(uid, state);
927             });
928
929             updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
930                     String.format("Error (%s) with write. Request: %s. Description: %s. Message: %s",
931                             error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
932         }
933     }
934
935     public synchronized void onWriteResponse(AsyncModbusWriteResult result) {
936         if (hasConfigurationError()) {
937             return;
938         } else if (!isWriteEnabled) {
939             return;
940         }
941         logger.debug("Successful write, matching request {}", result.getRequest());
942         updateStatusIfChanged(ThingStatus.ONLINE);
943         ChannelUID lastWriteSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_SUCCESS);
944         if (isLinked(lastWriteSuccessUID)) {
945             updateState(lastWriteSuccessUID, new DateTimeType());
946         }
947     }
948
949     /**
950      * Update linked channels
951      *
952      * @param numericState numeric state corresponding to polled data (or UNDEF with floating point NaN or infinity)
953      * @param boolValue boolean value corresponding to polled data
954      * @return updated channel data
955      */
956     private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) {
957         ModbusTransformation localReadTransformation = readTransformation;
958         if (localReadTransformation == null) {
959             // We should always have transformation available if thing is initalized properly
960             logger.trace("No transformation available, aborting processUpdatedValue");
961             return Collections.emptyMap();
962         }
963         Map<ChannelUID, State> states = new HashMap<>();
964         CHANNEL_ID_TO_ACCEPTED_TYPES.keySet().stream().forEach(channelId -> {
965             ChannelUID channelUID = getChannelUID(channelId);
966             if (!isLinked(channelUID)) {
967                 return;
968             }
969             List<Class<? extends State>> acceptedDataTypes = CHANNEL_ID_TO_ACCEPTED_TYPES.get(channelId);
970             if (acceptedDataTypes.isEmpty()) {
971                 return;
972             }
973
974             State boolLikeState;
975             if (containsOnOff(acceptedDataTypes)) {
976                 boolLikeState = OnOffType.from(boolValue);
977             } else if (containsOpenClosed(acceptedDataTypes)) {
978                 boolLikeState = boolValue ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
979             } else {
980                 boolLikeState = null;
981             }
982
983             State transformedState;
984             if (localReadTransformation.isIdentityTransform()) {
985                 if (boolLikeState != null) {
986                     // A bit of smartness for ON/OFF and OPEN/CLOSED with boolean like items
987                     transformedState = boolLikeState;
988                 } else {
989                     // Numeric states always go through transformation. This allows value of 17.5 to be
990                     // converted to
991                     // 17.5% with percent types (instead of raising error)
992                     transformedState = localReadTransformation.transformState(acceptedDataTypes, numericState);
993                 }
994             } else {
995                 transformedState = localReadTransformation.transformState(acceptedDataTypes, numericState);
996             }
997
998             if (transformedState != null) {
999                 if (logger.isTraceEnabled()) {
1000                     logger.trace(
1001                             "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
1002                             channelId, transformedState, transformedState.getClass().getSimpleName(), numericState,
1003                             readValueType, boolValue,
1004                             localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
1005                 }
1006                 states.put(channelUID, transformedState);
1007             } else {
1008                 String types = String.join(", ",
1009                         acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(String[]::new));
1010                 logger.warn(
1011                         "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: {}",
1012                         channelId, types, numericState, readValueType, boolValue,
1013                         localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
1014             }
1015         });
1016
1017         ChannelUID lastReadSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_SUCCESS);
1018         if (isLinked(lastReadSuccessUID)) {
1019             states.put(lastReadSuccessUID, new DateTimeType());
1020         }
1021         updateExpiredChannels(states);
1022         return states;
1023     }
1024
1025     private void updateExpiredChannels(Map<ChannelUID, State> states) {
1026         synchronized (this) {
1027             updateStatusIfChanged(ThingStatus.ONLINE);
1028             long now = System.currentTimeMillis();
1029             // Update channels that have not been updated in a while, or when their values has changed
1030             states.forEach((uid, state) -> updateExpiredChannel(now, uid, state));
1031             channelLastState = states;
1032         }
1033     }
1034
1035     // since lastState can be null, and "lastState == null" in conditional is not useless
1036     @SuppressWarnings("null")
1037     private void updateExpiredChannel(long now, ChannelUID uid, State state) {
1038         @Nullable
1039         State lastState = channelLastState.get(uid);
1040         long lastUpdatedMillis = channelLastUpdated.getOrDefault(uid, 0L);
1041         long millisSinceLastUpdate = now - lastUpdatedMillis;
1042         if (lastUpdatedMillis <= 0L || lastState == null || updateUnchangedValuesEveryMillis <= 0L
1043                 || millisSinceLastUpdate > updateUnchangedValuesEveryMillis || !lastState.equals(state)) {
1044             tryUpdateState(uid, state);
1045             channelLastUpdated.put(uid, now);
1046         }
1047     }
1048
1049     private void tryUpdateState(ChannelUID uid, State state) {
1050         try {
1051             updateState(uid, state);
1052         } catch (IllegalArgumentException e) {
1053             logger.warn("Error updating state '{}' (type {}) to channel {}: {} {}", state,
1054                     Optional.ofNullable(state).map(s -> s.getClass().getName()).orElse("null"), uid,
1055                     e.getClass().getName(), e.getMessage());
1056         }
1057     }
1058
1059     private ChannelUID getChannelUID(String channelID) {
1060         return Objects
1061                 .requireNonNull(channelCache.computeIfAbsent(channelID, id -> new ChannelUID(getThing().getUID(), id)));
1062     }
1063
1064     private void updateStatusIfChanged(ThingStatus status) {
1065         updateStatusIfChanged(status, ThingStatusDetail.NONE, null);
1066     }
1067
1068     private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail,
1069             @Nullable String description) {
1070         ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, description);
1071         Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
1072         boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
1073         if (statusInfo.getStatus() == ThingStatus.UNKNOWN || !statusInfo.equals(newStatusInfo) || intervalElapsed) {
1074             statusInfo = newStatusInfo;
1075             lastStatusInfoUpdate = LocalDateTime.now();
1076             updateStatus(newStatusInfo);
1077         }
1078     }
1079
1080     /**
1081      * Update status using pre-constructed ThingStatusInfo
1082      *
1083      * Implementation adapted from BaseThingHandler updateStatus implementations
1084      *
1085      * @param statusInfo new status info
1086      */
1087     protected void updateStatus(ThingStatusInfo statusInfo) {
1088         synchronized (this) {
1089             ThingHandlerCallback callback = getCallback();
1090             if (callback != null) {
1091                 callback.statusUpdated(this.thing, statusInfo);
1092             } else {
1093                 logger.warn("Handler {} tried updating the thing status although the handler was already disposed.",
1094                         this.getClass().getSimpleName());
1095             }
1096         }
1097     }
1098 }