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