From 90a608b3d00e6a6d2619a751517bd018d6bf8bc6 Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Sat, 7 Sep 2024 00:24:50 +1000 Subject: [PATCH] [modbus] Support chaining transformations without an intersection symbol (#17306) * [modbus] Refactor to use ChannelTransformation Signed-off-by: Jimmy Tanagra --- bundles/org.openhab.binding.modbus/README.md | 22 ++- .../CascadedValueTransformationImpl.java | 70 ------- .../modbus/internal/ModbusTransformation.java | 145 ++++++++++++++ .../internal/SingleValueTransformation.java | 179 ------------------ .../modbus/internal/ValueTransformation.java | 51 ----- .../config/ModbusDataConfiguration.java | 14 +- .../handler/ModbusDataThingHandler.java | 55 +++--- .../resources/OH-INF/i18n/modbus.properties | 19 +- .../resources/OH-INF/thing/thing-data.xml | 12 +- .../CascadedValueTransformationImplTest.java | 80 -------- .../internal/ModbusTransformationTest.java | 54 ++++++ .../SingleValueTransformationTest.java | 83 -------- 12 files changed, 268 insertions(+), 516 deletions(-) delete mode 100644 bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImpl.java create mode 100644 bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusTransformation.java delete mode 100644 bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java delete mode 100644 bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ValueTransformation.java delete mode 100644 bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImplTest.java create mode 100644 bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/ModbusTransformationTest.java delete mode 100644 bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/SingleValueTransformationTest.java diff --git a/bundles/org.openhab.binding.modbus/README.md b/bundles/org.openhab.binding.modbus/README.md index 5f00b3d56c3c8..3f1bc95830a39 100644 --- a/bundles/org.openhab.binding.modbus/README.md +++ b/bundles/org.openhab.binding.modbus/README.md @@ -201,11 +201,11 @@ 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"` 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. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. | +| `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"` 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. You 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). Value of `"bit"` can be used with registers as well when `writeStart` is of format `"X.Y"` (see below). 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. One can use `"X.Y"` to write individual bit `Y` of an holding `X` (analogous to `readStart`). | | `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"` 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"`. | +| `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"` 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. | @@ -555,8 +555,16 @@ Note that transformation is only one part of the overall process how polled data Consult [Read steps](#read-steps) and [Write steps](#write-steps) for more details. Specifically, note that you might not need transformations at all in some uses cases. +Transformations can be chained in the UI by listing each transformation on a separate line, or by separating them with the mathematical intersection character "∩". +In the .things file, multiple transformations can be specified by enclosing each transformation with double quotes, and separating them with commas, for example, +this will chain `JSONPATH` and `MAP` transformations: + +```java +Thing data DimmerReg [ ..., readTransform="JSONPATH($data)", "MAP(modbus_dimmer_read.map)", writeStart="4700", ... ] +``` + Please also note that you should install relevant transformations in openHAB as necessary. -For example, `openhab-transformation-javascript` feature provides the javascript (`JS`) transformation. +For example, [openhab-automation-jsscripting](/addons/automation/jsscripting/) feature provides the javascript (`JS`) transformation. #### Transform On Read @@ -565,7 +573,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 applying complex arithmetic of 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 applying complex arithmetic of 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. @@ -577,7 +585,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 applying complex arithmetic for 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 applying complex arithmetic for 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. #### Example: Inverting Binary Data On Read And Write @@ -841,7 +849,7 @@ Example for a dimmer device where 255 register value = 100% for fully ON: ```java 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)" ] } } ``` @@ -922,7 +930,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" ] 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 deleted file mode 100644 index 053671a5e038e..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2010-2024 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.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 = List.of(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/ModbusTransformation.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusTransformation.java new file mode 100644 index 0000000000000..9c0050f60a468 --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusTransformation.java @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2010-2024 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 java.util.Objects; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TypeParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * Class for performing transformations of a command or state. + * + * @author Jimmy Tanagra - Initial contribution + * + */ +@NonNullByDefault +public class ModbusTransformation { + + public static final String TRANSFORM_DEFAULT = "default"; + + /** + * Ordered list of types that are tried out first when trying to parse transformed command + */ + private static final List> DEFAULT_TYPES = List.of( // + DecimalType.class, // + OpenClosedType.class, // + OnOffType.class // + ); + + private final Logger logger = LoggerFactory.getLogger(ModbusTransformation.class); + private final @Nullable ChannelTransformation transformation; + private final @Nullable String constantOutput; + + /** + * Creates a transformation object. + * + * The transformations are chained and applied in the order they are given in the list. + * Each transformation can also contain the intersection symbol "∩" to separate + * multiple transformations in one line. + * + * - If the transformationList is null or consists of only blank strings, + * the output of the transformation will be an empty string regardless of the input. + * + * - If first element is "default", the transformation will be considered as + * an identity transformation, which returns the input as the output. + * Additional elements in the list are ignored. + * + * - If the transformationList contains valid transformation syntax, the output + * will be transformed according to the given transformations. + * + * - If the first element is some other value, it is treated as a constant and it + * will become the output of the transformation, regardless of the input. + * Additional elements in the list are ignored. + * + * @param transformations a list of transformations to apply. + */ + public ModbusTransformation(@Nullable List transformationList) { + if (transformationList == null || transformationList.isEmpty() + || transformationList.stream().allMatch(String::isBlank)) { + transformation = null; + constantOutput = ""; + return; + } + + int size = transformationList.size(); + String firstLine = transformationList.get(0).trim(); + + if (size == 1 && firstLine.equalsIgnoreCase(TRANSFORM_DEFAULT)) { + // no-op (identity) transformation + transformation = null; + constantOutput = null; + return; + } + + if (transformationList.stream().allMatch(ChannelTransformation::isValidTransformation)) { + transformation = new ChannelTransformation(transformationList); + constantOutput = null; + } else { + transformation = null; + constantOutput = firstLine; + if (size > 1) { + logger.warn( + "Given transformation configuration {} did not match the correct pattern. Transformation output will be constant '{}'", + transformationList, constantOutput); + } else { + logger.debug("The output for transformation {} will be constant '{}'", transformationList, + constantOutput); + } + } + } + + public String transform(String value) { + if (transformation != null) { + // return input if transformation failed + return Objects.requireNonNull(transformation.apply(value).orElse(value)); + } + + return Objects.requireNonNullElse(constantOutput, value); + } + + public boolean isIdentityTransform() { + return transformation == null && constantOutput == null; + } + + public static Optional tryConvertToCommand(String transformed) { + return Optional.ofNullable(TypeParser.parseCommand(DEFAULT_TYPES, transformed)); + } + + /** + * Transform state to another state using this transformation + * + * @param types types to used to parse the transformation result + * @param state + * @return Transformed command, or null if no transformation was possible + */ + public @Nullable State transformState(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(stateAsString); + return TypeParser.parseState(types, transformed); + } +} diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java deleted file mode 100644 index 68948366d6713..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/SingleValueTransformation.java +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) 2010-2024 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.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.OpenClosedType; -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.TypeParser; -import org.osgi.framework.BundleContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class describing transformation of a command or state. - * - * Inspired from other openHAB binding "Transformation" classes. - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public class SingleValueTransformation implements ValueTransformation { - - public static final String TRANSFORM_DEFAULT = "default"; - 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_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 - */ - private static final List> DEFAULT_TYPES = new ArrayList<>(); - static { - DEFAULT_TYPES.add(DecimalType.class); - DEFAULT_TYPES.add(OpenClosedType.class); - DEFAULT_TYPES.add(OnOffType.class); - } - - private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class); - - private final @Nullable String transformation; - final @Nullable String transformationServiceName; - final @Nullable String transformationServiceParam; - - /** - * - * @param transformation either FUN(VAL) (standard transformation syntax), default (identity transformation - * (output equals input)) or some other value (output is a constant). Futhermore, empty string is - * considered the same way as "default". - */ - public SingleValueTransformation(@Nullable String transformation) { - this.transformation = transformation; - // - // Parse transformation configuration here on construction, but delay the - // construction of TransformationService to call-time - if (transformation == null || transformation.isEmpty() || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) { - // no-op (identity) transformation - transformationServiceName = null; - transformationServiceParam = null; - } else { - 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(); - transformationServiceName = matcher.group("service"); - transformationServiceParam = matcher.group("arg"); - } else { - logger.debug( - "Given transformation configuration '{}' did not match the FUN(VAL) pattern. Transformation output will be constant '{}'", - transformation, transformation); - transformationServiceName = null; - transformationServiceParam = null; - } - } - } - - /** - * For testing, thus package visibility by design - * - * @param transformation - * @param transformationServiceName - * @param transformationServiceParam - */ - 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; - String transformationServiceParam = this.transformationServiceParam; - - if (transformationServiceName != null) { - try { - if (transformationServiceParam == null) { - throw new TransformationException( - "transformation service parameter is missing! Invalid transform?"); - } - @Nullable - TransformationService transformationService = TransformationHelper.getTransformationService(context, - transformationServiceName); - if (transformationService != null) { - transformedResponse = transformationService.transform(transformationServiceParam, value); - } else { - transformedResponse = value; - logger.warn("couldn't transform response because transformationService of type '{}' is unavailable", - transformationServiceName); - } - } catch (TransformationException te) { - logger.error("transformation throws exception [transformation={}, response={}]", transformation, value, - te); - - // in case of an error we return the response without any - // transformation - transformedResponse = value; - } - } else if (isIdentityTransform()) { - // identity transformation - transformedResponse = value; - } else { - // pass value as is - transformedResponse = this.transformation; - } - - return transformedResponse == null ? "" : transformedResponse; - } - - @Override - public boolean isIdentityTransform() { - return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation); - } - - public static Optional tryConvertToCommand(String transformed) { - return Optional.ofNullable(TypeParser.parseCommand(DEFAULT_TYPES, transformed)); - } - - @Override - public String 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 deleted file mode 100644 index e033d4666a516..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ValueTransformation.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2010-2024 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 state - * @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/config/ModbusDataConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java index eec9d4808d0c4..14152d792a8d9 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.modbus.internal.config; +import java.util.List; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,11 +27,11 @@ public class ModbusDataConfiguration { private @Nullable String readStart; - private @Nullable String readTransform; + private @Nullable List readTransform; private @Nullable String readValueType; private @Nullable String writeStart; private @Nullable String writeType; - private @Nullable String writeTransform; + private @Nullable List writeTransform; private @Nullable String writeValueType; private boolean writeMultipleEvenWithSingleRegisterOrCoil; private int writeMaxTries = 3; // backwards compatibility and tests @@ -43,11 +45,11 @@ public void setReadStart(String readStart) { this.readStart = readStart; } - public @Nullable String getReadTransform() { + public @Nullable List getReadTransform() { return readTransform; } - public void setReadTransform(String readTransform) { + public void setReadTransform(List readTransform) { this.readTransform = readTransform; } @@ -75,11 +77,11 @@ public void setWriteType(String writeType) { this.writeType = writeType; } - public @Nullable String getWriteTransform() { + public @Nullable List getWriteTransform() { return writeTransform; } - public void setWriteTransform(String writeTransform) { + public void setWriteTransform(List writeTransform) { this.writeTransform = writeTransform; } 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 75e6cc9de8dac..833d043f9fa22 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 @@ -32,11 +32,9 @@ 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.SingleValueTransformation; -import org.openhab.binding.modbus.internal.ValueTransformation; +import org.openhab.binding.modbus.internal.ModbusTransformation; import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration; import org.openhab.core.io.transport.modbus.AsyncModbusFailure; import org.openhab.core.io.transport.modbus.AsyncModbusReadResult; @@ -80,8 +78,6 @@ import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.openhab.core.util.HexUtils; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,8 +97,6 @@ public class ModbusDataThingHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class); - private final BundleContext bundleContext; - private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1); private static final Map>> CHANNEL_ID_TO_ACCEPTED_TYPES = new HashMap<>(); @@ -131,8 +125,8 @@ public class ModbusDataThingHandler extends BaseThingHandler { private volatile @Nullable ModbusDataConfiguration config; private volatile @Nullable ValueType readValueType; private volatile @Nullable ValueType writeValueType; - private volatile @Nullable CascadedValueTransformationImpl readTransformation; - private volatile @Nullable CascadedValueTransformationImpl writeTransformation; + private volatile @Nullable ModbusTransformation readTransformation; + private volatile @Nullable ModbusTransformation writeTransformation; private volatile Optional readIndex = Optional.empty(); private volatile Optional readSubIndex = Optional.empty(); private volatile Optional writeStart = Optional.empty(); @@ -158,7 +152,6 @@ public class ModbusDataThingHandler extends BaseThingHandler { public ModbusDataThingHandler(Thing thing) { super(thing); - this.bundleContext = FrameworkUtil.getBundle(ModbusDataThingHandler.class).getBundleContext(); } @Override @@ -242,11 +235,11 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { private @Nullable Optional transformCommandAndProcessJSON(ChannelUID channelUID, Command command) { String transformOutput; Optional transformedCommand; - ValueTransformation writeTransformation = this.writeTransformation; + ModbusTransformation writeTransformation = this.writeTransformation; if (writeTransformation == null || writeTransformation.isIdentityTransform()) { transformedCommand = Optional.of(command); } else { - transformOutput = writeTransformation.transform(bundleContext, command.toString()); + transformOutput = writeTransformation.transform(command.toString()); if (transformOutput.contains("[")) { processJsonTransform(command, transformOutput); return null; @@ -256,10 +249,12 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { command, channelUID)); return null; } else { - 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("")); + transformedCommand = ModbusTransformation.tryConvertToCommand(transformOutput); + if (logger.isTraceEnabled()) { + logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput, + transformedCommand.map(c -> c.toString()).orElse(""), + transformedCommand.map(c -> c.getClass().getName()).orElse("")); + } } } return transformedCommand; @@ -576,7 +571,7 @@ private void validateAndParseReadParameters(ModbusDataConfiguration config) thro throw new ModbusConfigurationException(errmsg); } } - readTransformation = new CascadedValueTransformationImpl(config.getReadTransform()); + readTransformation = new ModbusTransformation(config.getReadTransform()); validateReadIndex(); } @@ -584,8 +579,10 @@ private void validateAndParseWriteParameters(ModbusDataConfiguration config) thr boolean writeTypeMissing = config.getWriteType() == null || config.getWriteType().isBlank(); 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 CascadedValueTransformationImpl(config.getWriteTransform()); + boolean writeTransformationMissing = config.getWriteTransform() == null + || String.join("", Objects.requireNonNull(config.getWriteTransform())).isBlank(); + + writeTransformation = new ModbusTransformation(config.getWriteTransform()); boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType()); writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing && !writeTransformationMissing); @@ -957,7 +954,7 @@ public synchronized void onWriteResponse(AsyncModbusWriteResult result) { * @return updated channel data */ private Map processUpdatedValue(State numericState, boolean boolValue) { - ValueTransformation localReadTransformation = readTransformation; + ModbusTransformation localReadTransformation = readTransformation; if (localReadTransformation == null) { // We should always have transformation available if thing is initalized properly logger.trace("No transformation available, aborting processUpdatedValue"); @@ -992,20 +989,20 @@ private Map processUpdatedValue(State numericState, boolean b // Numeric states always go through transformation. This allows value of 17.5 to be // converted to // 17.5% with percent types (instead of raising error) - transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes, - numericState); + transformedState = localReadTransformation.transformState(acceptedDataTypes, numericState); } } else { - transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes, - numericState); + transformedState = localReadTransformation.transformState(acceptedDataTypes, numericState); } if (transformedState != null) { - logger.trace( - "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}", - channelId, transformedState, transformedState.getClass().getSimpleName(), numericState, - readValueType, boolValue, - localReadTransformation.isIdentityTransform() ? "" : localReadTransformation); + if (logger.isTraceEnabled()) { + logger.trace( + "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}", + channelId, transformedState, transformedState.getClass().getSimpleName(), numericState, + readValueType, boolValue, + localReadTransformation.isIdentityTransform() ? "" : localReadTransformation); + } states.put(channelUID, transformedState); } else { String types = String.join(", ", diff --git a/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/i18n/modbus.properties b/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/i18n/modbus.properties index 808c541c8d8b1..de915aa77f456 100644 --- a/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/i18n/modbus.properties +++ b/bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/i18n/modbus.properties @@ -19,7 +19,7 @@ thing-type.modbus.tcp.description = Endpoint for Modbus TCP slaves thing-type.config.modbus.data.readStart.label = Read Address thing-type.config.modbus.data.readStart.description = 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:
  • For example, 3.1 would mean pick second bit from register index 3 with bit value type.
  • With int8 valuetype, it would pick the high byte of register index 3.
thing-type.config.modbus.data.readTransform.label = Read Transform -thing-type.config.modbus.data.readTransform.description = 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 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.
You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2 +thing-type.config.modbus.data.readTransform.description = 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 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.
Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩", for example SERVICE1(ARG1)∩SERVICE2(ARG2) thing-type.config.modbus.data.readValueType.label = Read Value Type thing-type.config.modbus.data.readValueType.description = How data is read from modbus. Use empty for write-only things.

With registers all value types are applicable. thing-type.config.modbus.data.readValueType.option.int64 = 64bit signed integer (int64) @@ -46,7 +46,7 @@ thing-type.config.modbus.data.writeMultipleEvenWithSingleRegisterOrCoil.descript thing-type.config.modbus.data.writeStart.label = Write Address thing-type.config.modbus.data.writeStart.description = 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.
One can write individual bits of a register using X.Y format where X is the register and Y is the bit (0 refers to least significant bit). thing-type.config.modbus.data.writeTransform.label = Write Transform -thing-type.config.modbus.data.writeTransform.description = 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 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. +thing-type.config.modbus.data.writeTransform.description = 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 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 value is ignored.
Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩", for example SERVICE1(ARG1)∩SERVICE2(ARG2) thing-type.config.modbus.data.writeType.label = Write Type thing-type.config.modbus.data.writeType.description = Type of data to write. Leave empty for read-only things.

Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See writeMultipleEvenWithSingleRegisterOrCoil parameter. thing-type.config.modbus.data.writeType.option.coil = coil, or digital out (DO) @@ -114,13 +114,13 @@ thing-type.config.modbus.serial.encoding.option.bin = BIN thing-type.config.modbus.serial.flowControlIn.label = Flow Control In thing-type.config.modbus.serial.flowControlIn.description = Type of flow control for receiving thing-type.config.modbus.serial.flowControlIn.option.none = None -thing-type.config.modbus.serial.flowControlIn.option.xon/xoff in = XON/XOFF -thing-type.config.modbus.serial.flowControlIn.option.rts/cts in = RTS/CTS +thing-type.config.modbus.serial.flowControlIn.option.xon/xoff\ in = XON/XOFF +thing-type.config.modbus.serial.flowControlIn.option.rts/cts\ in = RTS/CTS thing-type.config.modbus.serial.flowControlOut.label = Flow Control Out thing-type.config.modbus.serial.flowControlOut.description = Type of flow control for sending thing-type.config.modbus.serial.flowControlOut.option.none = None -thing-type.config.modbus.serial.flowControlOut.option.xon/xoff out = XON/XOFF -thing-type.config.modbus.serial.flowControlOut.option.rts/cts out = RTS/CTS +thing-type.config.modbus.serial.flowControlOut.option.xon/xoff\ out = XON/XOFF +thing-type.config.modbus.serial.flowControlOut.option.rts/cts\ out = RTS/CTS thing-type.config.modbus.serial.id.label = Id thing-type.config.modbus.serial.id.description = Slave id. Also known as station address or unit identifier. thing-type.config.modbus.serial.parity.label = Parity @@ -186,3 +186,10 @@ channel-type.modbus.string-type.label = Value as String channel-type.modbus.string-type.description = String item channel channel-type.modbus.switch-type.label = Value as Switch channel-type.modbus.switch-type.description = Switch item channel + +# thing types config + +thing-type.config.modbus.serial.flowControlIn.option.xon/xoff in = XON/XOFF +thing-type.config.modbus.serial.flowControlIn.option.rts/cts in = RTS/CTS +thing-type.config.modbus.serial.flowControlOut.option.xon/xoff out = XON/XOFF +thing-type.config.modbus.serial.flowControlOut.option.rts/cts out = RTS/CTS 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 f063aa846b30d..c5d1f0f3680cf 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 @@ -40,14 +40,15 @@ ]]> - +
Use "default" to communicate that no transformation is done and value should be passed as is.
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. -
You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2]]>
+
Multiple transformations can be chained by listing each transformation on a separate line, + or by concatenating them with "∩", for example SERVICE1(ARG1)∩SERVICE2(ARG2)]]> default
@@ -96,14 +97,15 @@ - +
Use "default" to communicate that no transformation is done and value should be passed as is.
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.]]>
+ value is ignored. +
Multiple transformations can be chained by listing each transformation on a separate line, + or by concatenating them with "∩", for example SERVICE1(ARG1)∩SERVICE2(ARG2)]]> 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 deleted file mode 100644 index 04fff95de3e42..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/CascadedValueTransformationImplTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2010-2024 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.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.osgi.framework.BundleContext; - -/** - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -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/ModbusTransformationTest.java b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/ModbusTransformationTest.java new file mode 100644 index 0000000000000..30480e39e7ec7 --- /dev/null +++ b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/ModbusTransformationTest.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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 java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Jimmy Tanagra - Initial contribution + */ +@NonNullByDefault +public class ModbusTransformationTest { + @Test + public void testTransformationEmpty() { + ModbusTransformation transformation = new ModbusTransformation(List.of("")); + assertFalse(transformation.isIdentityTransform()); + assertEquals("", transformation.transform("xx")); + } + + @Test + public void testTransformationNull() { + ModbusTransformation transformation = new ModbusTransformation(null); + assertFalse(transformation.isIdentityTransform()); + assertEquals("", transformation.transform("xx")); + } + + @Test + public void testTransformationDefault() { + ModbusTransformation transformation = new ModbusTransformation(List.of("deFault")); + assertTrue(transformation.isIdentityTransform()); + assertEquals("xx", transformation.transform("xx")); + } + + @Test + public void testTransformationConstant() { + ModbusTransformation transformation = new ModbusTransformation(List.of("constant")); + assertFalse(transformation.isIdentityTransform()); + assertEquals("constant", transformation.transform("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 deleted file mode 100644 index b1744b40ab13b..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/SingleValueTransformationTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2010-2024 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.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.osgi.framework.BundleContext; - -/** - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -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")); - } -}