[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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user