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