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

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

Signed-off-by: Sami Salonen <ssalonen@gmail.com>
This commit is contained in:
Sami Salonen
2021-02-02 09:33:07 +02:00
committed by GitHub
parent 0be7f60438
commit 51ea77f022
10 changed files with 379 additions and 104 deletions

View File

@@ -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

View File

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

View File

@@ -12,17 +12,12 @@
*/
package org.openhab.binding.modbus.internal;
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 + "]";
}
}

View File

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

View File

@@ -31,9 +31,11 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.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");

View File

@@ -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>