diff --git a/bundles/org.openhab.binding.modbus/README.md b/bundles/org.openhab.binding.modbus/README.md index c2a0f6c0d..c6f561aa4 100644 --- a/bundles/org.openhab.binding.modbus/README.md +++ b/bundles/org.openhab.binding.modbus/README.md @@ -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.

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.

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).

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: | -| `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`.

Use "default" to communicate that no transformation is done and value should be passed as is.
Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`.
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`.

Use "default" to communicate that no transformation is done and value should be passed as is.
Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`.
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). | | `writeStart` | text | | (empty) | Start address of the first holding register or coil in the write. Use empty for read-only things.
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"`.

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.

Use `"default"` to communicate that no transformation is done and value should be passed as is.
Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`.
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. | -| `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.
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"). | +| `writeTransform` | text | | `"default"` | Transformation to apply to received commands.

Use `"default"` to communicate that no transformation is done and value should be passed as is.
Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`.
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.
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

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.

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: 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. 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: 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. #### Transformation Example: Scaling @@ -777,7 +777,7 @@ This example divides value on read, and multiplies them on write, using JS trans ```xtend Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] { 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 Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] { 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" ] { // Since we are using advanced transformation outputting JSON, // 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 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 -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. 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). We now have to create `Things` for this slaves. -The 2.x modbus binding uses a three-level definition. -Level one defines a `Bridge` for every modbus device that is to be addressed. +The 2.x modbus binding uses a three-level definition. +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. ``` @@ -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. 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. -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. 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): ``` -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. -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`: ``` @@ -1202,32 +1202,32 @@ Bridge modbus:tcp:wago [ host="192.168.2.9", port=502, id=1 ] { } ``` -Save this in the `things` folder. -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`. +Save this in the `things` folder. +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`. -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: ``` -Switch FooSwitch "Foo Switch" {modbus="slave1:0"} +Switch FooSwitch "Foo Switch" {modbus="slave1:0"} Switch BarSwitch "Bar Switch" {modbus="slave1:1"} ``` 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"} ``` 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:` -by +by `{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. Similar expressions are to be used for two-digit and three-digit relative addresses. -Replace +Replace `"}` -by +by `: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. Save your updated item file and check whether updates come in as expected. - + ## Troubleshooting ### Thing Status diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java index d8832d435..12f18c765 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java @@ -60,7 +60,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler { * bridge. This makes sense, as the callback delegates * to all child things of this bridge. * - * @author Sami Salonen + * @author Sami Salonen - Initial contribution * */ private class ReadCallbackDelegator diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImpl.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImpl.java new file mode 100644 index 000000000..ec644c151 --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImpl.java @@ -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 transformations; + + public CascadedValueTransformationImpl(@Nullable String transformationString) { + String transformationNonNull = transformationString == null ? "" : transformationString; + List 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 getTransformations() { + return transformations; + } +} diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java similarity index 66% rename from bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java rename to bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java index f73bff5b1..bc90119f9 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java @@ -12,17 +12,12 @@ */ package org.openhab.binding.modbus.internal; -import static org.apache.commons.lang.StringUtils.isEmpty; - import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; 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.Nullable; 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.TransformationService; import org.openhab.core.types.Command; -import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.osgi.framework.BundleContext; import org.slf4j.Logger; @@ -47,13 +41,15 @@ import org.slf4j.LoggerFactory; * */ @NonNullByDefault -public class Transformation { +public class SingleValueTransformation implements ValueTransformation { 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 '(.*?)\((.*)\)' */ - private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(?.*?)\\((?.*)\\)"); + private static final Pattern EXTRACT_FUNCTION_PATTERN_OLD = Pattern.compile("(?.*?)\\((?.*)\\)"); + private static final Pattern EXTRACT_FUNCTION_PATTERN_NEW = Pattern.compile("(?.*?):(?.*)"); /** * 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); } - private final Logger logger = LoggerFactory.getLogger(Transformation.class); - - private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); - - static { - toStringStyle.setUseShortClassName(true); - } + private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class); private final @Nullable String transformation; - private final @Nullable String transformationServiceName; - private final @Nullable String transformationServiceParam; + final @Nullable String transformationServiceName; + 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 * considered the same way as "default". */ - public Transformation(@Nullable String transformation) { + public SingleValueTransformation(@Nullable String transformation) { this.transformation = transformation; // // Parse transformation configuration here on construction, but delay the // 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 transformationServiceName = null; transformationServiceParam = null; } 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()) { matcher.reset(); matcher.find(); @@ -116,13 +114,14 @@ public class Transformation { * @param transformationServiceName * @param transformationServiceParam */ - Transformation(String transformation, @Nullable String transformationServiceName, + SingleValueTransformation(String transformation, @Nullable String transformationServiceName, @Nullable String transformationServiceParam) { this.transformation = transformation; this.transformationServiceName = transformationServiceName; this.transformationServiceParam = transformationServiceParam; } + @Override public String transform(BundleContext context, String value) { String transformedResponse; String transformationServiceName = this.transformationServiceName; @@ -163,6 +162,7 @@ public class Transformation { return transformedResponse == null ? "" : transformedResponse; } + @Override public boolean isIdentityTransform() { return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation); } @@ -172,52 +172,9 @@ public class Transformation { 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> 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 public String toString() { - return new ToStringBuilder(this, toStringStyle).append("tranformation", transformation) - .append("transformationServiceName", transformationServiceName) - .append("transformationServiceParam", transformationServiceParam).toString(); + return "SingleValueTransformation [transformation=" + transformation + ", transformationServiceName=" + + transformationServiceName + ", transformationServiceParam=" + transformationServiceParam + "]"; } } diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ValueTransformation.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ValueTransformation.java new file mode 100644 index 000000000..219ad57ef --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ValueTransformation.java @@ -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> 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); + } +} diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java index d9f416e6d..9b6c22327 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java @@ -31,9 +31,11 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.modbus.handler.EndpointNotInitializedException; import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; 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.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.core.io.transport.modbus.AsyncModbusFailure; 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 ValueType readValueType; private volatile @Nullable ValueType writeValueType; - private volatile @Nullable Transformation readTransformation; - private volatile @Nullable Transformation writeTransformation; + private volatile @Nullable CascadedValueTransformationImpl readTransformation; + private volatile @Nullable CascadedValueTransformationImpl writeTransformation; private volatile Optional readIndex = Optional.empty(); private volatile Optional readSubIndex = Optional.empty(); private volatile @Nullable Integer writeStart; @@ -237,7 +239,7 @@ public class ModbusDataThingHandler extends BaseThingHandler { private @Nullable Optional transformCommandAndProcessJSON(ChannelUID channelUID, Command command) { String transformOutput; Optional transformedCommand; - Transformation writeTransformation = this.writeTransformation; + ValueTransformation writeTransformation = this.writeTransformation; if (writeTransformation == null || writeTransformation.isIdentityTransform()) { transformedCommand = Optional.of(command); } else { @@ -251,7 +253,7 @@ public class ModbusDataThingHandler extends BaseThingHandler { command, channelUID)); return null; } else { - transformedCommand = Transformation.tryConvertToCommand(transformOutput); + transformedCommand = SingleValueTransformation.tryConvertToCommand(transformOutput); logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput, transformedCommand.map(c -> c.toString()).orElse(""), transformedCommand.map(c -> c.getClass().getName()).orElse("")); @@ -502,7 +504,7 @@ public class ModbusDataThingHandler extends BaseThingHandler { throw new ModbusConfigurationException(errmsg); } } - readTransformation = new Transformation(config.getReadTransform()); + readTransformation = new CascadedValueTransformationImpl(config.getReadTransform()); validateReadIndex(); } @@ -511,7 +513,7 @@ public class ModbusDataThingHandler extends BaseThingHandler { boolean writeStartMissing = config.getWriteStart() == null || config.getWriteStart().isBlank(); boolean writeValueTypeMissing = config.getWriteValueType() == null || config.getWriteValueType().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()); writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing && !writeTransformationMissing); @@ -818,7 +820,7 @@ public class ModbusDataThingHandler extends BaseThingHandler { * @return updated channel data */ private Map processUpdatedValue(State numericState, boolean boolValue) { - Transformation localReadTransformation = readTransformation; + ValueTransformation localReadTransformation = readTransformation; if (localReadTransformation == null) { // We should always have transformation available if thing is initalized properly logger.trace("No transformation available, aborting processUpdatedValue"); diff --git a/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/thing/thing-data.xml b/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/thing/thing-data.xml index c6220eef3..df2937230 100644 --- a/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/thing/thing-data.xml +++ b/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/thing/thing-data.xml @@ -44,9 +44,10 @@
Use "default" to communicate that no transformation is done and value should be passed as is. -
Use SERVICENAME(ARG) to use transformation service. +
Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
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.]]>
+ value is ignored. +
You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2]]> default @@ -97,8 +98,9 @@
Use "default" to communicate that no transformation is done and value should be passed as is. -
Use SERVICENAME(ARG) to use transformation service. +
Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
Any other value than the above types will be interpreted as static text, in which case the actual content of the command +
You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2 value is ignored.]]>
default
diff --git a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImplTest.java b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImplTest.java new file mode 100644 index 000000000..303c62b2d --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImplTest.java @@ -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")); + } +} diff --git a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/SingleValueTransformationTest.java b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/SingleValueTransformationTest.java new file mode 100644 index 000000000..504f7dd10 --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/SingleValueTransformationTest.java @@ -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")); + } +} diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java index a45d73458..5cd510e09 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java @@ -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, ThingStatus expectedStatus) { ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);