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