[modbus] Modbus transformations: cascaded/chained transformations and new-style transformation string (#9945)
* [modbus] Cascaded transforms with ∩ * [modbus] README to mention cascaded transformations * [modbus] Take cascaded transformation into use * [modbus] README to show preference towards new syntax * [modbus] examples to use new syntax * [modbus] fix test * [modbus] remove apache commons lang dependency - see also PR #10002 - I removed equals and hashCode implementation all-together, I could not see they played any role in practice. Signed-off-by: Sami Salonen <ssalonen@gmail.com>
This commit is contained in:
parent
0be7f60438
commit
51ea77f022
|
@ -202,12 +202,12 @@ You must give each of your data Things a reference (thing ID) that is unique for
|
|||
| ------------------------------------------- | ------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `readValueType` | text | | (empty) | How data is read from modbus. Use empty for write-only things.<br /><br />Bit value type must be used with coils and discrete inputs. With registers all value types are applicable. Valid values are: `"int64"`, `"int64_swap"`, `"uint64"`, `"uint64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"uint32"`, `"uint32_swap"`, `"int16"`, `"uint16"`, `"int8"`, `"uint8"`, or `"bit"`. See also [Value types on read and write](#value-types-on-read-and-write). |
|
||||
| `readStart` | text | | (empty) | Start address to start reading the value. Use empty for write-only things. <br /><br />Input as zero-based index number, e.g. in place of `400001` (first holding register), use the address `"0"`. Must be between (poller start) and (poller start + poller length - 1) (inclusive).<br /><br />With registers and value type less than 16 bits, you must use `"X.Y"` format where `Y` specifies the sub-element to read from the 16 bit register:<ul> <li>For example, `"3.1"` would mean pick second bit from register index `3` with bit value type. </li><li>With int8 valuetype, it would pick the high byte of register index `3`.</li></ul> |
|
||||
| `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled value is ignored. |
|
||||
| `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled value is ignored. ou can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
|
||||
| `writeValueType` | text | | (empty) | How data is written to modbus. Only applicable to registers. Valid values are: `"int64"`, `"int64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"int16"`. See also [Value types on read and write](#value-types-on-read-and-write). |
|
||||
| `writeStart` | text | | (empty) | Start address of the first holding register or coil in the write. Use empty for read-only things. <br />Use zero based address, e.g. in place of `400001` (first holding register), use the address `"0"`. This address is passed to data frame as is. |
|
||||
| `writeType` | text | | (empty) | Type of data to write. Use empty for read-only things. Valid values: `"coil"` or `"holding"`.<br /><br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See `writeMultipleEvenWithSingleRegisterOrCoil` parameter. |
|
||||
| `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME(ARG)"` to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command value is ignored. |
|
||||
| `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). |
|
||||
| `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command value is ignored. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
|
||||
| `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). |
|
||||
| `writeMaxTries` | integer | | `3` | Maximum tries when writing <br /><br />Number of tries when writing data, if some of the writes fail. For single try, enter `1`. |
|
||||
| `updateUnchangedValuesEveryMillis` | integer | | `1000` | Interval to update unchanged values. <br /><br />Modbus binding by default is not updating the item and channel state every time new data is polled from a slave, for performance reasons. Instead, the state is updated whenever it differs from previously updated state, or when enough time has passed since the last update. The time interval can be adjusted using this parameter. Use value of `0` if you like to update state with every poll, even though the value has not changed. In milliseconds. |
|
||||
|
||||
|
@ -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" ]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.modbus.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.osgi.framework.BundleContext;
|
||||
|
||||
/**
|
||||
* The {@link CascadedValueTransformationImpl} implements {@link SingleValueTransformation for a cascaded set of
|
||||
* transformations}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
* @author Sami Salonen - Copied from HTTP binding to provide consistent user experience
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CascadedValueTransformationImpl implements ValueTransformation {
|
||||
private final List<SingleValueTransformation> transformations;
|
||||
|
||||
public CascadedValueTransformationImpl(@Nullable String transformationString) {
|
||||
String transformationNonNull = transformationString == null ? "" : transformationString;
|
||||
List<SingleValueTransformation> localTransformations = Arrays.stream(transformationNonNull.split("∩"))
|
||||
.filter(s -> !s.isEmpty()).map(transformation -> new SingleValueTransformation(transformation))
|
||||
.collect(Collectors.toList());
|
||||
if (localTransformations.isEmpty()) {
|
||||
localTransformations = Collections.singletonList(new SingleValueTransformation(transformationString));
|
||||
}
|
||||
transformations = localTransformations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String transform(BundleContext context, String value) {
|
||||
String input = value;
|
||||
// process all transformations
|
||||
for (final ValueTransformation transformation : transformations) {
|
||||
input = transformation.transform(context, input);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdentityTransform() {
|
||||
return transformations.stream().allMatch(SingleValueTransformation::isIdentityTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CascadedValueTransformationImpl("
|
||||
+ transformations.stream().map(SingleValueTransformation::toString).collect(Collectors.joining(" ∩ "))
|
||||
+ ")";
|
||||
}
|
||||
|
||||
List<SingleValueTransformation> getTransformations() {
|
||||
return transformations;
|
||||
}
|
||||
}
|
|
@ -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 <code>'(.*?)\((.*)\)'</code> */
|
||||
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
|
||||
private static final Pattern EXTRACT_FUNCTION_PATTERN_OLD = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
|
||||
private static final Pattern EXTRACT_FUNCTION_PATTERN_NEW = Pattern.compile("(?<service>.*?):(?<arg>.*)");
|
||||
|
||||
/**
|
||||
* Ordered list of types that are tried out first when trying to parse transformed command
|
||||
|
@ -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<Class<? extends State>> types, State state) {
|
||||
// Note that even identity transformations go through the State -> String -> State steps. This does add some
|
||||
// overhead but takes care of DecimalType -> PercentType conversions, for example.
|
||||
final String stateAsString = state.toString();
|
||||
final String transformed = transform(context, stateAsString);
|
||||
return TypeParser.parseState(types, transformed);
|
||||
}
|
||||
|
||||
public boolean hasTransformationService() {
|
||||
return transformationServiceName != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (null == obj) {
|
||||
return false;
|
||||
}
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Transformation)) {
|
||||
return false;
|
||||
}
|
||||
Transformation that = (Transformation) obj;
|
||||
EqualsBuilder eb = new EqualsBuilder();
|
||||
if (hasTransformationService()) {
|
||||
eb.append(this.transformationServiceName, that.transformationServiceName);
|
||||
eb.append(this.transformationServiceParam, that.transformationServiceParam);
|
||||
} else {
|
||||
eb.append(this.transformation, that.transformation);
|
||||
}
|
||||
return eb.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
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 + "]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.modbus.internal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.osgi.framework.BundleContext;
|
||||
|
||||
/**
|
||||
* Interface for Transformation
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ValueTransformation {
|
||||
|
||||
String transform(BundleContext context, String value);
|
||||
|
||||
boolean isIdentityTransform();
|
||||
|
||||
/**
|
||||
* Transform state to another state using this transformation
|
||||
*
|
||||
* @param context
|
||||
* @param types types to used to parse the transformation result
|
||||
* @param command
|
||||
* @return Transformed command, or null if no transformation was possible
|
||||
*/
|
||||
default @Nullable State transformState(BundleContext context, List<Class<? extends State>> types, State state) {
|
||||
// Note that even identity transformations go through the State -> String -> State steps. This does add some
|
||||
// overhead but takes care of DecimalType -> PercentType conversions, for example.
|
||||
final String stateAsString = state.toString();
|
||||
final String transformed = transform(context, stateAsString);
|
||||
return TypeParser.parseState(types, transformed);
|
||||
}
|
||||
}
|
|
@ -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<Integer> readIndex = Optional.empty();
|
||||
private volatile Optional<Integer> readSubIndex = Optional.empty();
|
||||
private volatile @Nullable Integer writeStart;
|
||||
|
@ -237,7 +239,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
|
|||
private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
|
||||
String transformOutput;
|
||||
Optional<Command> 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("<conversion failed>"),
|
||||
transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
|
||||
|
@ -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<ChannelUID, State> 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");
|
||||
|
|
|
@ -44,9 +44,10 @@
|
|||
<label>Read Transform</label>
|
||||
<description><![CDATA[Transformation to apply to polled data, after it has been converted to number using readValueType
|
||||
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
|
||||
<br />Use SERVICENAME(ARG) to use transformation service.
|
||||
<br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
|
||||
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled
|
||||
value is ignored.]]></description>
|
||||
value is ignored.
|
||||
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2]]></description>
|
||||
<default>default</default>
|
||||
</parameter>
|
||||
<parameter name="readValueType" type="text">
|
||||
|
@ -97,8 +98,9 @@
|
|||
<label>Write Transform</label>
|
||||
<description><![CDATA[Transformation to apply to received commands.
|
||||
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
|
||||
<br />Use SERVICENAME(ARG) to use transformation service.
|
||||
<br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
|
||||
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command
|
||||
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2
|
||||
value is ignored.]]></description>
|
||||
<default>default</default>
|
||||
</parameter>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue