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