[modbus] Modbus transformations: cascaded/chained transformations and new-style transformation string (#9945)

* [modbus] Cascaded transforms with ∩
* [modbus] README to mention cascaded transformations
* [modbus] Take cascaded transformation into use
* [modbus] README to show preference towards new syntax
* [modbus] examples to use new syntax
* [modbus] fix test
* [modbus] remove apache commons lang dependency
- see also PR #10002
- I removed equals and hashCode implementation all-together, I could not see they played any role in practice.

Signed-off-by: Sami Salonen <ssalonen@gmail.com>
This commit is contained in:
Sami Salonen 2021-02-02 09:33:07 +02:00 committed by GitHub
parent 0be7f60438
commit 51ea77f022
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 379 additions and 104 deletions

View File

@ -202,12 +202,12 @@ You must give each of your data Things a reference (thing ID) that is unique for
| ------------------------------------------- | ------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------- | ------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readValueType` | text | | (empty) | How data is read from modbus. Use empty for write-only things.<br /><br />Bit value type must be used with coils and discrete inputs. With registers all value types are applicable. Valid values are: `"int64"`, `"int64_swap"`, `"uint64"`, `"uint64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"uint32"`, `"uint32_swap"`, `"int16"`, `"uint16"`, `"int8"`, `"uint8"`, or `"bit"`. See also [Value types on read and write](#value-types-on-read-and-write). | | `readValueType` | text | | (empty) | How data is read from modbus. Use empty for write-only things.<br /><br />Bit value type must be used with coils and discrete inputs. With registers all value types are applicable. Valid values are: `"int64"`, `"int64_swap"`, `"uint64"`, `"uint64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"uint32"`, `"uint32_swap"`, `"int16"`, `"uint16"`, `"int8"`, `"uint8"`, or `"bit"`. See also [Value types on read and write](#value-types-on-read-and-write). |
| `readStart` | text | | (empty) | Start address to start reading the value. Use empty for write-only things. <br /><br />Input as zero-based index number, e.g. in place of `400001` (first holding register), use the address `"0"`. Must be between (poller start) and (poller start + poller length - 1) (inclusive).<br /><br />With registers and value type less than 16 bits, you must use `"X.Y"` format where `Y` specifies the sub-element to read from the 16 bit register:<ul> <li>For example, `"3.1"` would mean pick second bit from register index `3` with bit value type. </li><li>With int8 valuetype, it would pick the high byte of register index `3`.</li></ul> | | `readStart` | text | | (empty) | Start address to start reading the value. Use empty for write-only things. <br /><br />Input as zero-based index number, e.g. in place of `400001` (first holding register), use the address `"0"`. Must be between (poller start) and (poller start + poller length - 1) (inclusive).<br /><br />With registers and value type less than 16 bits, you must use `"X.Y"` format where `Y` specifies the sub-element to read from the 16 bit register:<ul> <li>For example, `"3.1"` would mean pick second bit from register index `3` with bit value type. </li><li>With int8 valuetype, it would pick the high byte of register index `3`.</li></ul> |
| `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled value is ignored. | | `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled value is ignored. ou can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
| `writeValueType` | text | | (empty) | How data is written to modbus. Only applicable to registers. Valid values are: `"int64"`, `"int64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"int16"`. See also [Value types on read and write](#value-types-on-read-and-write). | | `writeValueType` | text | | (empty) | How data is written to modbus. Only applicable to registers. Valid values are: `"int64"`, `"int64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"int16"`. See also [Value types on read and write](#value-types-on-read-and-write). |
| `writeStart` | text | | (empty) | Start address of the first holding register or coil in the write. Use empty for read-only things. <br />Use zero based address, e.g. in place of `400001` (first holding register), use the address `"0"`. This address is passed to data frame as is. | | `writeStart` | text | | (empty) | Start address of the first holding register or coil in the write. Use empty for read-only things. <br />Use zero based address, e.g. in place of `400001` (first holding register), use the address `"0"`. This address is passed to data frame as is. |
| `writeType` | text | | (empty) | Type of data to write. Use empty for read-only things. Valid values: `"coil"` or `"holding"`.<br /><br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See `writeMultipleEvenWithSingleRegisterOrCoil` parameter. | | `writeType` | text | | (empty) | Type of data to write. Use empty for read-only things. Valid values: `"coil"` or `"holding"`.<br /><br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See `writeMultipleEvenWithSingleRegisterOrCoil` parameter. |
| `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command value is ignored. | | `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command value is ignored. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
| `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). | | `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). |
| `writeMaxTries` | integer | | `3` | Maximum tries when writing <br /><br />Number of tries when writing data, if some of the writes fail. For single try, enter `1`. | | `writeMaxTries` | integer | | `3` | Maximum tries when writing <br /><br />Number of tries when writing data, if some of the writes fail. For single try, enter `1`. |
| `updateUnchangedValuesEveryMillis` | integer | | `1000` | Interval to update unchanged values. <br /><br />Modbus binding by default is not updating the item and channel state every time new data is polled from a slave, for performance reasons. Instead, the state is updated whenever it differs from previously updated state, or when enough time has passed since the last update. The time interval can be adjusted using this parameter. Use value of `0` if you like to update state with every poll, even though the value has not changed. In milliseconds. | | `updateUnchangedValuesEveryMillis` | integer | | `1000` | Interval to update unchanged values. <br /><br />Modbus binding by default is not updating the item and channel state every time new data is polled from a slave, for performance reasons. Instead, the state is updated whenever it differs from previously updated state, or when enough time has passed since the last update. The time interval can be adjusted using this parameter. Use value of `0` if you like to update state with every poll, even though the value has not changed. In milliseconds. |
@ -551,7 +551,7 @@ For example, `openhab-transformation-javascript` feature provides the javascript
There are three different format to specify the configuration: There are three different format to specify the configuration:
1. String `"default"`, in which case the default transformation is used. The default is to convert non-zero numbers to `ON`/`OPEN`, and zero numbers to `OFF`/`CLOSED`, respectively. If the item linked to the data channel does not accept these states, the number is converted to best-effort-basis to the states accepted by the item. For example, the extracted number is passed as-is for `Number` items, while `ON`/`OFF` would be used with `DimmerItem`. 1. String `"default"`, in which case the default transformation is used. The default is to convert non-zero numbers to `ON`/`OPEN`, and zero numbers to `OFF`/`CLOSED`, respectively. If the item linked to the data channel does not accept these states, the number is converted to best-effort-basis to the states accepted by the item. For example, the extracted number is passed as-is for `Number` items, while `ON`/`OFF` would be used with `DimmerItem`.
1. `"SERVICENAME(ARG)"` for calling a transformation service. The transformation receives the extracted number as input. This is useful for example scaling (divide by x) the polled data before it is used in openHAB. See examples for more details. 1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the extracted number as input. This is useful for example scaling (divide by x) the polled data before it is used in openHAB. See examples for more details.
1. Any other value is interpreted as static text, in which case the actual content of the polled value is ignored. Transformation result is always the same. The transformation output is converted to best-effort-basis to the states accepted by the item. 1. Any other value is interpreted as static text, in which case the actual content of the polled value is ignored. Transformation result is always the same. The transformation output is converted to best-effort-basis to the states accepted by the item.
Consult [background documentation on items](https://www.openhab.org/docs/concepts/items.html) to understand accepted data types (state) by each item. Consult [background documentation on items](https://www.openhab.org/docs/concepts/items.html) to understand accepted data types (state) by each item.
@ -563,7 +563,7 @@ Consult [background documentation on items](https://www.openhab.org/docs/concept
There are three different format to specify the configuration: There are three different format to specify the configuration:
1. String `"default"`, in which case the default transformation is used. The default is to do no conversion to the command. 1. String `"default"`, in which case the default transformation is used. The default is to do no conversion to the command.
1. `"SERVICENAME(ARG)"` for calling a transformation service. The transformation receives the command as input. This is useful for example scaling ("multiply by x") commands before the data is written to Modbus. See examples for more details. 1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the command as input. This is useful for example scaling ("multiply by x") commands before the data is written to Modbus. See examples for more details.
1. Any other value is interpreted as static text, in which case the actual command is ignored. Transformation result is always the same. 1. Any other value is interpreted as static text, in which case the actual command is ignored. Transformation result is always the same.
#### Transformation Example: Scaling #### Transformation Example: Scaling
@ -777,7 +777,7 @@ This example divides value on read, and multiplies them on write, using JS trans
```xtend ```xtend
Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] { Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] {
Bridge poller holdingPoller [ start=5, length=1, refresh=5000, type="holding" ] { Bridge poller holdingPoller [ start=5, length=1, refresh=5000, type="holding" ] {
Thing data holding5Scaled [ readStart="5", readValueType="int16", readTransform="JS(divide10.js)", writeStart="5", writeValueType="int16", writeType="holding", writeTransform="JS(multiply10.js)" ] Thing data holding5Scaled [ readStart="5", readValueType="int16", readTransform="JS:divide10.js", writeStart="5", writeValueType="int16", writeType="holding", writeTransform="JS:multiply10.js" ]
} }
} }
``` ```
@ -815,7 +815,7 @@ Example for a dimmer device where 255 register value = 100% for fully ON:
```xtend ```xtend
Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] { Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] {
Bridge poller MBDimmer [ start=4700, length=2, refresh=1000, type="holding" ] { Bridge poller MBDimmer [ start=4700, length=2, refresh=1000, type="holding" ] {
Thing data DimmerReg [ readStart="4700", readValueType="uint16", readTransform="JS(dimread255.js)", writeStart="4700", writeValueType="uint16", writeType="holding", writeTransform="JS(dimwrite255.js)" ] Thing data DimmerReg [ readStart="4700", readValueType="uint16", readTransform="JS:dimread255.js", writeStart="4700", writeValueType="uint16", writeType="holding", writeTransform="JS:dimwrite255.js" ]
} }
} }
``` ```
@ -895,7 +895,7 @@ Bridge modbus:tcp:localhostTCPRollerShutter [ host="127.0.0.1", port=502 ] {
Bridge poller holding [ start=0, length=3, refresh=1000, type="holding" ] { Bridge poller holding [ start=0, length=3, refresh=1000, type="holding" ] {
// Since we are using advanced transformation outputting JSON, // Since we are using advanced transformation outputting JSON,
// other write parameters (writeValueType, writeStart, writeType) can be omitted // other write parameters (writeValueType, writeStart, writeType) can be omitted
Thing data rollershutterData [ readStart="0", readValueType="int16", writeTransform="JS(rollershutter.js)" ] Thing data rollershutterData [ readStart="0", readValueType="int16", writeTransform="JS:rollershutter.js" ]
// For diagnostics // For diagnostics
Thing data rollershutterDebug0 [ readStart="0", readValueType="int16", writeStart="0", writeValueType="int16", writeType="holding" ] Thing data rollershutterDebug0 [ readStart="0", readValueType="int16", writeStart="0", writeValueType="int16", writeType="holding" ]
@ -1069,7 +1069,7 @@ The new binding supports 32 and 64 bit values types when writing.
### How to manually migrate ### How to manually migrate
Here is a step by step example for a migration from a 1.x configuration to an equivalent 2.x configuration. Here is a step by step example for a migration from a 1.x configuration to an equivalent 2.x configuration.
It does not cover all features the 1.x configuration offers, but it should serve as an example on how to get it done. It does not cover all features the 1.x configuration offers, but it should serve as an example on how to get it done.
The 1.x modbus configuration to be updated defined 4 slaves: The 1.x modbus configuration to be updated defined 4 slaves:
@ -1107,8 +1107,8 @@ The 1.x modbus configuration to be updated defined 4 slaves:
As you can see, all the slaves poll the same modbus device (actually a Wago 750-841 controller). As you can see, all the slaves poll the same modbus device (actually a Wago 750-841 controller).
We now have to create `Things` for this slaves. We now have to create `Things` for this slaves.
The 2.x modbus binding uses a three-level definition. The 2.x modbus binding uses a three-level definition.
Level one defines a `Bridge` for every modbus device that is to be addressed. Level one defines a `Bridge` for every modbus device that is to be addressed.
The 1.x configuration in this example only addresses one device, so there will be one top level bridge. The 1.x configuration in this example only addresses one device, so there will be one top level bridge.
``` ```
@ -1145,7 +1145,7 @@ Address, length and type can be directly taken over from the 1.x config.
The third (and most complex) part is the definition of data `Thing` objects for every `Item` bound to modbus. The third (and most complex) part is the definition of data `Thing` objects for every `Item` bound to modbus.
This definitions go into the corresponding 2nd level `Bridge` definitions. This definitions go into the corresponding 2nd level `Bridge` definitions.
Here it is especially important that the modbus binding now uses absolute addresses all over the place, while the addresses in the item definition for the 1.x binding were relative to the start address of the slave definition before. Here it is especially important that the modbus binding now uses absolute addresses all over the place, while the addresses in the item definition for the 1.x binding were relative to the start address of the slave definition before.
For less work in the following final step, the update of the `Item` configuration, the naming of the `data` things in this example uses the offset of the modbus value within the `poller` as suffix, starting with 0(!). For less work in the following final step, the update of the `Item` configuration, the naming of the `data` things in this example uses the offset of the modbus value within the `poller` as suffix, starting with 0(!).
See below for details. See below for details.
Here a few examples of the Item configuration from the 1.x binding: Here a few examples of the Item configuration from the 1.x binding:
@ -1153,12 +1153,12 @@ Here a few examples of the Item configuration from the 1.x binding:
The first Item polled with the first `poller` used this configuration (with offset 0): The first Item polled with the first `poller` used this configuration (with offset 0):
``` ```
Switch FooSwitch "Foo Switch" {modbus="slave1:0"} Switch FooSwitch "Foo Switch" {modbus="slave1:0"}
``` ```
Now we have to define a `Thing` that can later be bound to that Item. Now we have to define a `Thing` that can later be bound to that Item.
The `slave1` `poller` uses `12288` as start address. The `slave1` `poller` uses `12288` as start address.
So we define the first data Thing within the `poller` `wago_slave1` with this address and choose a name that ends with `0`: So we define the first data Thing within the `poller` `wago_slave1` with this address and choose a name that ends with `0`:
``` ```
@ -1202,32 +1202,32 @@ Bridge modbus:tcp:wago [ host="192.168.2.9", port=502, id=1 ] {
} }
``` ```
Save this in the `things` folder. Save this in the `things` folder.
Watch the file `events.log` as it lists your new added `data` `Things`. Watch the file `events.log` as it lists your new added `data` `Things`.
Given that there are no config errors, they quickly change from `INITIALIZING` to `ONLINE`. Given that there are no config errors, they quickly change from `INITIALIZING` to `ONLINE`.
Finally the Item definition has to be changed to refer to the new created `data` `Thing`. Finally the Item definition has to be changed to refer to the new created `data` `Thing`.
You can copy the names you need for this directly from the `events.log` file: You can copy the names you need for this directly from the `events.log` file:
``` ```
Switch FooSwitch "Foo Switch" {modbus="slave1:0"} Switch FooSwitch "Foo Switch" {modbus="slave1:0"}
Switch BarSwitch "Bar Switch" {modbus="slave1:1"} Switch BarSwitch "Bar Switch" {modbus="slave1:1"}
``` ```
turn into turn into
``` ```
Switch FooSwitch "Foo Switch" {channel="modbus:data:wago:wago_slave1:wago_s1_000:switch", autopudate="false"} Switch FooSwitch "Foo Switch" {channel="modbus:data:wago:wago_slave1:wago_s1_000:switch", autopudate="false"}
Switch BarSwitch "Bar Switch" {channel="modbus:data:wago:wago_slave1:wago_s1_001:switch", autoupdate="false"} Switch BarSwitch "Bar Switch" {channel="modbus:data:wago:wago_slave1:wago_s1_001:switch", autoupdate="false"}
``` ```
If you have many Items to change and used the naming scheme recommended above, you can now use the following search-and-replace expressions in your editor: If you have many Items to change and used the naming scheme recommended above, you can now use the following search-and-replace expressions in your editor:
Replace Replace
`{modbus="slave1:` `{modbus="slave1:`
by by
`{channel="modbus:data:wago:wago_slave1:wago_s1_00` `{channel="modbus:data:wago:wago_slave1:wago_s1_00`
@ -1235,11 +1235,11 @@ in all lines which used single digits for the address in the 1.x config.
Instead of `wago`, `wago_slave1` and `wago_s1_00` you have to use the names you have chosen for your `Bridge`, `poller` and `data` things. Instead of `wago`, `wago_slave1` and `wago_s1_00` you have to use the names you have chosen for your `Bridge`, `poller` and `data` things.
Similar expressions are to be used for two-digit and three-digit relative addresses. Similar expressions are to be used for two-digit and three-digit relative addresses.
Replace Replace
`"}` `"}`
by by
`:switch"}` `:switch"}`
@ -1253,7 +1253,7 @@ The definition of `autoupdate` is optional; please refer to [`autoupdate`](#auto
Continue to add `data` `Thing`s for all your other Items the same way and link them to your Items. Continue to add `data` `Thing`s for all your other Items the same way and link them to your Items.
Save your updated item file and check whether updates come in as expected. Save your updated item file and check whether updates come in as expected.
## Troubleshooting ## Troubleshooting
### Thing Status ### Thing Status

View File

@ -60,7 +60,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
* bridge. This makes sense, as the callback delegates * bridge. This makes sense, as the callback delegates
* to all child things of this bridge. * to all child things of this bridge.
* *
* @author Sami Salonen * @author Sami Salonen - Initial contribution
* *
*/ */
private class ReadCallbackDelegator private class ReadCallbackDelegator

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.framework.BundleContext;
/**
* The {@link CascadedValueTransformationImpl} implements {@link SingleValueTransformation for a cascaded set of
* transformations}
*
* @author Jan N. Klug - Initial contribution
* @author Sami Salonen - Copied from HTTP binding to provide consistent user experience
*/
@NonNullByDefault
public class CascadedValueTransformationImpl implements ValueTransformation {
private final List<SingleValueTransformation> transformations;
public CascadedValueTransformationImpl(@Nullable String transformationString) {
String transformationNonNull = transformationString == null ? "" : transformationString;
List<SingleValueTransformation> localTransformations = Arrays.stream(transformationNonNull.split(""))
.filter(s -> !s.isEmpty()).map(transformation -> new SingleValueTransformation(transformation))
.collect(Collectors.toList());
if (localTransformations.isEmpty()) {
localTransformations = Collections.singletonList(new SingleValueTransformation(transformationString));
}
transformations = localTransformations;
}
@Override
public String transform(BundleContext context, String value) {
String input = value;
// process all transformations
for (final ValueTransformation transformation : transformations) {
input = transformation.transform(context, input);
}
return input;
}
@Override
public boolean isIdentityTransform() {
return transformations.stream().allMatch(SingleValueTransformation::isIdentityTransform);
}
@Override
public String toString() {
return "CascadedValueTransformationImpl("
+ transformations.stream().map(SingleValueTransformation::toString).collect(Collectors.joining(""))
+ ")";
}
List<SingleValueTransformation> getTransformations() {
return transformations;
}
}

View File

@ -12,17 +12,12 @@
*/ */
package org.openhab.binding.modbus.internal; package org.openhab.binding.modbus.internal;
import static org.apache.commons.lang.StringUtils.isEmpty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
@ -32,7 +27,6 @@ import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService; import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser; import org.openhab.core.types.TypeParser;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -47,13 +41,15 @@ import org.slf4j.LoggerFactory;
* *
*/ */
@NonNullByDefault @NonNullByDefault
public class Transformation { public class SingleValueTransformation implements ValueTransformation {
public static final String TRANSFORM_DEFAULT = "default"; public static final String TRANSFORM_DEFAULT = "default";
public static final Transformation IDENTITY_TRANSFORMATION = new Transformation(TRANSFORM_DEFAULT, null, null); public static final ValueTransformation IDENTITY_TRANSFORMATION = new SingleValueTransformation(TRANSFORM_DEFAULT,
null, null);
/** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */ /** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)"); private static final Pattern EXTRACT_FUNCTION_PATTERN_OLD = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
private static final Pattern EXTRACT_FUNCTION_PATTERN_NEW = Pattern.compile("(?<service>.*?):(?<arg>.*)");
/** /**
* Ordered list of types that are tried out first when trying to parse transformed command * Ordered list of types that are tried out first when trying to parse transformed command
@ -65,17 +61,11 @@ public class Transformation {
DEFAULT_TYPES.add(OnOffType.class); DEFAULT_TYPES.add(OnOffType.class);
} }
private final Logger logger = LoggerFactory.getLogger(Transformation.class); private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class);
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final @Nullable String transformation; private final @Nullable String transformation;
private final @Nullable String transformationServiceName; final @Nullable String transformationServiceName;
private final @Nullable String transformationServiceParam; final @Nullable String transformationServiceParam;
/** /**
* *
@ -83,17 +73,25 @@ public class Transformation {
* (output equals input)) or some other value (output is a constant). Futhermore, empty string is * (output equals input)) or some other value (output is a constant). Futhermore, empty string is
* considered the same way as "default". * considered the same way as "default".
*/ */
public Transformation(@Nullable String transformation) { public SingleValueTransformation(@Nullable String transformation) {
this.transformation = transformation; this.transformation = transformation;
// //
// Parse transformation configuration here on construction, but delay the // Parse transformation configuration here on construction, but delay the
// construction of TransformationService to call-time // construction of TransformationService to call-time
if (isEmpty(transformation) || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) { if (transformation == null || transformation.isEmpty() || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) {
// no-op (identity) transformation // no-op (identity) transformation
transformationServiceName = null; transformationServiceName = null;
transformationServiceParam = null; transformationServiceParam = null;
} else { } else {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation); int colonIndex = transformation.indexOf(":");
int parenthesisOpenIndex = transformation.indexOf("(");
final Matcher matcher;
if (parenthesisOpenIndex != -1 && (colonIndex == -1 || parenthesisOpenIndex < colonIndex)) {
matcher = EXTRACT_FUNCTION_PATTERN_OLD.matcher(transformation);
} else {
matcher = EXTRACT_FUNCTION_PATTERN_NEW.matcher(transformation);
}
if (matcher.matches()) { if (matcher.matches()) {
matcher.reset(); matcher.reset();
matcher.find(); matcher.find();
@ -116,13 +114,14 @@ public class Transformation {
* @param transformationServiceName * @param transformationServiceName
* @param transformationServiceParam * @param transformationServiceParam
*/ */
Transformation(String transformation, @Nullable String transformationServiceName, SingleValueTransformation(String transformation, @Nullable String transformationServiceName,
@Nullable String transformationServiceParam) { @Nullable String transformationServiceParam) {
this.transformation = transformation; this.transformation = transformation;
this.transformationServiceName = transformationServiceName; this.transformationServiceName = transformationServiceName;
this.transformationServiceParam = transformationServiceParam; this.transformationServiceParam = transformationServiceParam;
} }
@Override
public String transform(BundleContext context, String value) { public String transform(BundleContext context, String value) {
String transformedResponse; String transformedResponse;
String transformationServiceName = this.transformationServiceName; String transformationServiceName = this.transformationServiceName;
@ -163,6 +162,7 @@ public class Transformation {
return transformedResponse == null ? "" : transformedResponse; return transformedResponse == null ? "" : transformedResponse;
} }
@Override
public boolean isIdentityTransform() { public boolean isIdentityTransform() {
return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation); return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation);
} }
@ -172,52 +172,9 @@ public class Transformation {
return transformedCommand; return transformedCommand;
} }
/**
* Transform state to another state using this transformation
*
* @param context
* @param types types to used to parse the transformation result
* @param command
* @return Transformed command, or null if no transformation was possible
*/
public @Nullable State transformState(BundleContext context, List<Class<? extends State>> types, State state) {
// Note that even identity transformations go through the State -> String -> State steps. This does add some
// overhead but takes care of DecimalType -> PercentType conversions, for example.
final String stateAsString = state.toString();
final String transformed = transform(context, stateAsString);
return TypeParser.parseState(types, transformed);
}
public boolean hasTransformationService() {
return transformationServiceName != null;
}
@Override
public boolean equals(@Nullable Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Transformation)) {
return false;
}
Transformation that = (Transformation) obj;
EqualsBuilder eb = new EqualsBuilder();
if (hasTransformationService()) {
eb.append(this.transformationServiceName, that.transformationServiceName);
eb.append(this.transformationServiceParam, that.transformationServiceParam);
} else {
eb.append(this.transformation, that.transformation);
}
return eb.isEquals();
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, toStringStyle).append("tranformation", transformation) return "SingleValueTransformation [transformation=" + transformation + ", transformationServiceName="
.append("transformationServiceName", transformationServiceName) + transformationServiceName + ", transformationServiceParam=" + transformationServiceParam + "]";
.append("transformationServiceParam", transformationServiceParam).toString();
} }
} }

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
import org.osgi.framework.BundleContext;
/**
* Interface for Transformation
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public interface ValueTransformation {
String transform(BundleContext context, String value);
boolean isIdentityTransform();
/**
* Transform state to another state using this transformation
*
* @param context
* @param types types to used to parse the transformation result
* @param command
* @return Transformed command, or null if no transformation was possible
*/
default @Nullable State transformState(BundleContext context, List<Class<? extends State>> types, State state) {
// Note that even identity transformations go through the State -> String -> State steps. This does add some
// overhead but takes care of DecimalType -> PercentType conversions, for example.
final String stateAsString = state.toString();
final String transformed = transform(context, stateAsString);
return TypeParser.parseState(types, transformed);
}
}

View File

@ -31,9 +31,11 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException; import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
import org.openhab.binding.modbus.internal.CascadedValueTransformationImpl;
import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal; import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
import org.openhab.binding.modbus.internal.ModbusConfigurationException; import org.openhab.binding.modbus.internal.ModbusConfigurationException;
import org.openhab.binding.modbus.internal.Transformation; import org.openhab.binding.modbus.internal.SingleValueTransformation;
import org.openhab.binding.modbus.internal.ValueTransformation;
import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration; import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration;
import org.openhab.core.io.transport.modbus.AsyncModbusFailure; import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
import org.openhab.core.io.transport.modbus.AsyncModbusReadResult; import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
@ -127,8 +129,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
private volatile @Nullable ModbusDataConfiguration config; private volatile @Nullable ModbusDataConfiguration config;
private volatile @Nullable ValueType readValueType; private volatile @Nullable ValueType readValueType;
private volatile @Nullable ValueType writeValueType; private volatile @Nullable ValueType writeValueType;
private volatile @Nullable Transformation readTransformation; private volatile @Nullable CascadedValueTransformationImpl readTransformation;
private volatile @Nullable Transformation writeTransformation; private volatile @Nullable CascadedValueTransformationImpl writeTransformation;
private volatile Optional<Integer> readIndex = Optional.empty(); private volatile Optional<Integer> readIndex = Optional.empty();
private volatile Optional<Integer> readSubIndex = Optional.empty(); private volatile Optional<Integer> readSubIndex = Optional.empty();
private volatile @Nullable Integer writeStart; private volatile @Nullable Integer writeStart;
@ -237,7 +239,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) { private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
String transformOutput; String transformOutput;
Optional<Command> transformedCommand; Optional<Command> transformedCommand;
Transformation writeTransformation = this.writeTransformation; ValueTransformation writeTransformation = this.writeTransformation;
if (writeTransformation == null || writeTransformation.isIdentityTransform()) { if (writeTransformation == null || writeTransformation.isIdentityTransform()) {
transformedCommand = Optional.of(command); transformedCommand = Optional.of(command);
} else { } else {
@ -251,7 +253,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
command, channelUID)); command, channelUID));
return null; return null;
} else { } else {
transformedCommand = Transformation.tryConvertToCommand(transformOutput); transformedCommand = SingleValueTransformation.tryConvertToCommand(transformOutput);
logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput, logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput,
transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"), transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"),
transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>")); transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
@ -502,7 +504,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
throw new ModbusConfigurationException(errmsg); throw new ModbusConfigurationException(errmsg);
} }
} }
readTransformation = new Transformation(config.getReadTransform()); readTransformation = new CascadedValueTransformationImpl(config.getReadTransform());
validateReadIndex(); validateReadIndex();
} }
@ -511,7 +513,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank(); boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank();
boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().isBlank(); boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().isBlank();
boolean writeTransformationMissing = config.getWriteTransform() == null || config.getWriteTransform().isBlank(); boolean writeTransformationMissing = config.getWriteTransform() == null || config.getWriteTransform().isBlank();
writeTransformation = new Transformation(config.getWriteTransform()); writeTransformation = new CascadedValueTransformationImpl(config.getWriteTransform());
boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType()); boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType());
writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing
&& !writeTransformationMissing); && !writeTransformationMissing);
@ -818,7 +820,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
* @return updated channel data * @return updated channel data
*/ */
private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) { private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) {
Transformation localReadTransformation = readTransformation; ValueTransformation localReadTransformation = readTransformation;
if (localReadTransformation == null) { if (localReadTransformation == null) {
// We should always have transformation available if thing is initalized properly // We should always have transformation available if thing is initalized properly
logger.trace("No transformation available, aborting processUpdatedValue"); logger.trace("No transformation available, aborting processUpdatedValue");

View File

@ -44,9 +44,10 @@
<label>Read Transform</label> <label>Read Transform</label>
<description><![CDATA[Transformation to apply to polled data, after it has been converted to number using readValueType <description><![CDATA[Transformation to apply to polled data, after it has been converted to number using readValueType
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
<br />Use SERVICENAME(ARG) to use transformation service. <br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled
value is ignored.]]></description> value is ignored.
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2]]></description>
<default>default</default> <default>default</default>
</parameter> </parameter>
<parameter name="readValueType" type="text"> <parameter name="readValueType" type="text">
@ -97,8 +98,9 @@
<label>Write Transform</label> <label>Write Transform</label>
<description><![CDATA[Transformation to apply to received commands. <description><![CDATA[Transformation to apply to received commands.
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
<br />Use SERVICENAME(ARG) to use transformation service. <br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2
value is ignored.]]></description> value is ignored.]]></description>
<default>default</default> <default>default</default>
</parameter> </parameter>

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.osgi.framework.BundleContext;
/**
* @author Sami Salonen - Initial contribution
*/
public class CascadedValueTransformationImplTest {
@Test
public void testTransformation() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(
"REGEX(myregex:foo(.*))∩REG_(EX(myregex:foo(.*))∩JIHAA:test");
assertEquals(3, transformation.getTransformations().size());
assertEquals("REGEX", transformation.getTransformations().get(0).transformationServiceName);
assertEquals("myregex:foo(.*)", transformation.getTransformations().get(0).transformationServiceParam);
assertEquals("REG_", transformation.getTransformations().get(1).transformationServiceName);
assertEquals("EX(myregex:foo(.*)", transformation.getTransformations().get(1).transformationServiceParam);
assertEquals("JIHAA", transformation.getTransformations().get(2).transformationServiceName);
assertEquals("test", transformation.getTransformations().get(2).transformationServiceParam);
assertEquals(3, transformation.toString().split("").length);
}
@Test
public void testTransformationEmpty() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("");
assertFalse(transformation.isIdentityTransform());
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationNull() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(null);
assertFalse(transformation.isIdentityTransform());
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationDefault() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("deFault");
assertTrue(transformation.isIdentityTransform());
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationDefaultChained() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("deFault∩DEFAULT∩default");
assertTrue(transformation.isIdentityTransform());
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationDefaultChainedWithStatic() {
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(
"deFault∩DEFAULT∩default∩static");
assertFalse(transformation.isIdentityTransform());
assertEquals("static", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.osgi.framework.BundleContext;
/**
* @author Sami Salonen - Initial contribution
*/
public class SingleValueTransformationTest {
@Test
public void testTransformationOldStyle() {
SingleValueTransformation transformation = new SingleValueTransformation("REGEX(myregex:foo(.*))");
assertEquals("REGEX", transformation.transformationServiceName);
assertEquals("myregex:foo(.*)", transformation.transformationServiceParam);
}
@Test
public void testTransformationOldStyle2() {
SingleValueTransformation transformation = new SingleValueTransformation("REG_(EX(myregex:foo(.*))");
assertEquals("REG_", transformation.transformationServiceName);
assertEquals("EX(myregex:foo(.*)", transformation.transformationServiceParam);
}
@Test
public void testTransformationNewStyle() {
SingleValueTransformation transformation = new SingleValueTransformation("REGEX:myregex(.*)");
assertEquals("REGEX", transformation.transformationServiceName);
assertEquals("myregex(.*)", transformation.transformationServiceParam);
}
@Test
public void testTransformationNewStyle2() {
SingleValueTransformation transformation = new SingleValueTransformation("REGEX::myregex(.*)");
assertEquals("REGEX", transformation.transformationServiceName);
assertEquals(":myregex(.*)", transformation.transformationServiceParam);
}
@Test
public void testTransformationEmpty() {
SingleValueTransformation transformation = new SingleValueTransformation("");
assertFalse(transformation.isIdentityTransform());
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationNull() {
SingleValueTransformation transformation = new SingleValueTransformation(null);
assertFalse(transformation.isIdentityTransform());
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationDefault() {
SingleValueTransformation transformation = new SingleValueTransformation("deFault");
assertTrue(transformation.isIdentityTransform());
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
@Test
public void testTransformationDefaultChainedWithStatic() {
SingleValueTransformation transformation = new SingleValueTransformation("static");
assertFalse(transformation.isIdentityTransform());
assertEquals("static", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
}
}

View File

@ -714,6 +714,39 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
} }
} }
@Test
public void testWriteRealTransformation5() throws InvalidSyntaxException {
captureModbusWrites();
mockTransformation("PLUS", new TransformationService() {
@Override
public String transform(String arg, String source) throws TransformationException {
return String.valueOf(Integer.parseInt(arg) + Integer.parseInt(source));
}
});
mockTransformation("CONCAT", new TransformationService() {
@Override
public String transform(String function, String source) throws TransformationException {
return source + function;
}
});
mockTransformation("MULTIPLY", new MultiplyTransformation());
ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY:3∩PLUS(2)∩CONCAT(0)",
ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
new DecimalType("2"), null, bundleContext);
assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
assertThat(writeRequests.size(), is(equalTo(1)));
ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
assertThat(writeRequest.getReference(), is(equalTo(50)));
assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
is(equalTo(/* (2*3 + 2) + '0' */ 80)));
}
private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType, private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType,
ThingStatus expectedStatus) { ThingStatus expectedStatus) {
ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false); ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);