diff --git a/bundles/org.openhab.core.thing/pom.xml b/bundles/org.openhab.core.thing/pom.xml
index 58cd26a944..f969dcd210 100644
--- a/bundles/org.openhab.core.thing/pom.xml
+++ b/bundles/org.openhab.core.thing/pom.xml
@@ -25,6 +25,11 @@
org.openhab.core.io.console
${project.version}
+
+ org.openhab.core.bundles
+ org.openhab.core.transform
+ ${project.version}
+
org.openhab.core.bundles
org.openhab.core.test
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java
new file mode 100644
index 0000000000..8839167d4f
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link ChannelHandler} defines the interface for converting received {@link ChannelHandlerContent}
+ * to {@link org.openhab.core.types.State}s for posting updates to {@link org.openhab.core.thing.Channel}s and
+ * {@link Command}s to values for sending
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ChannelHandler {
+
+ /**
+ * called to process a given content for this channel
+ *
+ * @param content raw content to process (null
results in
+ * {@link org.openhab.core.types.UnDefType#UNDEF})
+ */
+ void process(@Nullable ChannelHandlerContent content);
+
+ /**
+ * called to send a command to this channel
+ *
+ * @param command
+ */
+ void send(Command command);
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java
new file mode 100644
index 0000000000..ed5b4ad3e1
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link ChannelHandlerContent} defines the pre-processed response
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelHandlerContent {
+ private final byte[] rawContent;
+ private final Charset encoding;
+ private final @Nullable String mediaType;
+
+ public ChannelHandlerContent(byte[] rawContent, String encoding, @Nullable String mediaType) {
+ this.rawContent = rawContent;
+ this.mediaType = mediaType;
+
+ Charset finalEncoding = StandardCharsets.UTF_8;
+ try {
+ finalEncoding = Charset.forName(encoding);
+ } catch (IllegalArgumentException e) {
+ }
+ this.encoding = finalEncoding;
+ }
+
+ public byte[] getRawContent() {
+ return rawContent;
+ }
+
+ public String getAsString() {
+ return new String(rawContent, encoding);
+ }
+
+ public @Nullable String getMediaType() {
+ return mediaType;
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java
new file mode 100644
index 0000000000..6335386674
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ChannelMode} enum defines control modes for channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum ChannelMode {
+ READONLY,
+ READWRITE,
+ WRITEONLY
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java
new file mode 100644
index 0000000000..e4187cdae1
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.transform.TransformationException;
+import org.openhab.core.transform.TransformationHelper;
+import org.openhab.core.transform.TransformationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ChannelTransformation} can be used to transform an input value using one or more transformations.
+ * Individual transformations can be chained with ∩
and must follow the pattern
+ * serviceName:function
where serviceName
refers to a {@link TransformationService} and
+ * function
has to be a valid transformation function for this service
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+public class ChannelTransformation {
+ private final Logger logger = LoggerFactory.getLogger(ChannelTransformation.class);
+ private List transformationSteps;
+
+ public ChannelTransformation(@Nullable String transformationString) {
+ if (transformationString != null) {
+ try {
+ transformationSteps = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isBlank())
+ .map(TransformationStep::new).toList();
+ return;
+ } catch (IllegalArgumentException e) {
+ logger.warn("Transformation ignored, failed to parse {}: {}", transformationString, e.getMessage());
+ }
+ }
+ transformationSteps = List.of();
+ }
+
+ public Optional apply(String value) {
+ Optional valueOptional = Optional.of(value);
+
+ // process all transformations
+ for (TransformationStep transformationStep : transformationSteps) {
+ valueOptional = valueOptional.flatMap(transformationStep::apply);
+ }
+
+ logger.trace("Transformed '{}' to '{}' using '{}'", value, valueOptional, transformationSteps);
+ return valueOptional;
+ }
+
+ private static class TransformationStep {
+ private final Logger logger = LoggerFactory.getLogger(TransformationStep.class);
+ private final String serviceName;
+ private final String function;
+
+ public TransformationStep(String pattern) throws IllegalArgumentException {
+ int index = pattern.indexOf(":");
+ if (index == -1) {
+ throw new IllegalArgumentException(
+ "The transformation pattern must consist of the type and the pattern separated by a colon");
+ }
+ this.serviceName = pattern.substring(0, index).toUpperCase().trim();
+ this.function = pattern.substring(index + 1).trim();
+ }
+
+ public Optional apply(String value) {
+ TransformationService service = TransformationHelper.getTransformationService(serviceName);
+ if (service != null) {
+ try {
+ return Optional.ofNullable(service.transform(function, value));
+ } catch (TransformationException e) {
+ logger.debug("Applying {} failed: {}", this, e.getMessage());
+ }
+ } else {
+ logger.warn("Failed to use {}, service not found", this);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public String toString() {
+ return "TransformationStep{serviceName='" + serviceName + "', function='" + function + "'}";
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java
new file mode 100644
index 0000000000..72dd8a8a05
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.binding.generic.converter.ColorChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ChannelValueConverterConfig} is a base class for the channel configuration of things
+ * using the {@link ChannelHandler}s
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ChannelValueConverterConfig {
+ private final Map stringStateMap = new HashMap<>();
+ private final Map commandStringMap = new HashMap<>();
+
+ public ChannelMode mode = ChannelMode.READWRITE;
+
+ // number
+ public @Nullable String unit;
+
+ // switch, dimmer, color
+ public @Nullable String onValue;
+ public @Nullable String offValue;
+
+ // dimmer, color
+ public BigDecimal step = BigDecimal.ONE;
+ public @Nullable String increaseValue;
+ public @Nullable String decreaseValue;
+
+ // color
+ public ColorChannelHandler.ColorMode colorMode = ColorChannelHandler.ColorMode.RGB;
+
+ // contact
+ public @Nullable String openValue;
+ public @Nullable String closedValue;
+
+ // rollershutter
+ public @Nullable String upValue;
+ public @Nullable String downValue;
+ public @Nullable String stopValue;
+ public @Nullable String moveValue;
+
+ // player
+ public @Nullable String playValue;
+ public @Nullable String pauseValue;
+ public @Nullable String nextValue;
+ public @Nullable String previousValue;
+ public @Nullable String rewindValue;
+ public @Nullable String fastforwardValue;
+
+ private boolean initialized = false;
+
+ /**
+ * maps a command to a user-defined string
+ *
+ * @param command the command to map
+ * @return a string or null if no mapping found
+ */
+ public @Nullable String commandToFixedValue(Command command) {
+ if (!initialized) {
+ createMaps();
+ }
+
+ return commandStringMap.get(command);
+ }
+
+ /**
+ * maps a user-defined string to a state
+ *
+ * @param string the string to map
+ * @return the state or null if no mapping found
+ */
+ public @Nullable State fixedValueToState(String string) {
+ if (!initialized) {
+ createMaps();
+ }
+
+ return stringStateMap.get(string);
+ }
+
+ private void createMaps() {
+ addToMaps(this.onValue, OnOffType.ON);
+ addToMaps(this.offValue, OnOffType.OFF);
+ addToMaps(this.openValue, OpenClosedType.OPEN);
+ addToMaps(this.closedValue, OpenClosedType.CLOSED);
+ addToMaps(this.upValue, UpDownType.UP);
+ addToMaps(this.downValue, UpDownType.DOWN);
+
+ commandStringMap.put(IncreaseDecreaseType.INCREASE, increaseValue);
+ commandStringMap.put(IncreaseDecreaseType.DECREASE, decreaseValue);
+ commandStringMap.put(StopMoveType.STOP, stopValue);
+ commandStringMap.put(StopMoveType.MOVE, moveValue);
+ commandStringMap.put(PlayPauseType.PLAY, playValue);
+ commandStringMap.put(PlayPauseType.PAUSE, pauseValue);
+ commandStringMap.put(NextPreviousType.NEXT, nextValue);
+ commandStringMap.put(NextPreviousType.PREVIOUS, previousValue);
+ commandStringMap.put(RewindFastforwardType.REWIND, rewindValue);
+ commandStringMap.put(RewindFastforwardType.FASTFORWARD, fastforwardValue);
+
+ initialized = true;
+ }
+
+ private void addToMaps(@Nullable String value, State state) {
+ if (value != null) {
+ commandStringMap.put((Command) state, value);
+ stringStateMap.put(value, state);
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java
new file mode 100644
index 0000000000..c2dff0ef26
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+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.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ColorChannelHandler} implements {@link org.openhab.core.library.items.ColorItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ColorChannelHandler extends AbstractTransformingChannelHandler {
+ private static final BigDecimal BYTE_FACTOR = BigDecimal.valueOf(2.55);
+ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+ private static final Pattern TRIPLE_MATCHER = Pattern.compile("(?\\d+),(?\\d+),(?\\d+)");
+
+ private State state = UnDefType.UNDEF;
+
+ public ColorChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof HSBType newState) {
+ state = newState;
+ return hsbToString(newState);
+ } else if (command instanceof PercentType percentCommand && state instanceof HSBType colorState) {
+ HSBType newState = new HSBType(colorState.getBrightness(), colorState.getSaturation(), percentCommand);
+ state = newState;
+ return hsbToString(newState);
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+ if (string.equals(channelConfig.onValue)) {
+ if (state instanceof HSBType) {
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ PercentType.HUNDRED);
+ } else {
+ newState = HSBType.WHITE;
+ }
+ } else if (string.equals(channelConfig.offValue)) {
+ if (state instanceof HSBType) {
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), PercentType.ZERO);
+ } else {
+ newState = HSBType.BLACK;
+ }
+ } else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType) {
+ BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().add(channelConfig.step);
+ if (HUNDRED.compareTo(newBrightness) < 0) {
+ newBrightness = HUNDRED;
+ }
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ new PercentType(newBrightness));
+ } else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType) {
+ BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().subtract(channelConfig.step);
+ if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+ newBrightness = BigDecimal.ZERO;
+ }
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ new PercentType(newBrightness));
+ } else {
+ Matcher matcher = TRIPLE_MATCHER.matcher(string);
+ if (matcher.matches()) {
+ switch (channelConfig.colorMode) {
+ case RGB -> {
+ int r = Integer.parseInt(matcher.group("r"));
+ int g = Integer.parseInt(matcher.group("g"));
+ int b = Integer.parseInt(matcher.group("b"));
+ newState = HSBType.fromRGB(r, g, b);
+ }
+ case HSB -> newState = new HSBType(string);
+ }
+ }
+ }
+
+ state = newState;
+ return Optional.of(newState);
+ }
+
+ private String hsbToString(HSBType state) {
+ switch (channelConfig.colorMode) {
+ case RGB:
+ PercentType[] rgb = state.toRGB();
+ return String.format("%1$d,%2$d,%3$d", rgb[0].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+ rgb[1].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+ rgb[2].toBigDecimal().multiply(BYTE_FACTOR).intValue());
+ case HSB:
+ return state.toString();
+ }
+ throw new IllegalStateException("Invalid colorMode setting");
+ }
+
+ public enum ColorMode {
+ RGB,
+ HSB
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java
new file mode 100644
index 0000000000..f9980de783
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link DimmerChannelHandler} implements {@link org.openhab.core.library.items.DimmerItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class DimmerChannelHandler extends AbstractTransformingChannelHandler {
+ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+ private State state = UnDefType.UNDEF;
+
+ public DimmerChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof PercentType) {
+ return ((PercentType) command).toString();
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+
+ if (string.equals(channelConfig.onValue)) {
+ newState = PercentType.HUNDRED;
+ } else if (string.equals(channelConfig.offValue)) {
+ newState = PercentType.ZERO;
+ } else if (string.equals(channelConfig.increaseValue) && state instanceof PercentType) {
+ BigDecimal newBrightness = ((PercentType) state).toBigDecimal().add(channelConfig.step);
+ if (HUNDRED.compareTo(newBrightness) < 0) {
+ newBrightness = HUNDRED;
+ }
+ newState = new PercentType(newBrightness);
+ } else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType) {
+ BigDecimal newBrightness = ((PercentType) state).toBigDecimal().subtract(channelConfig.step);
+ if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+ newBrightness = BigDecimal.ZERO;
+ }
+ newState = new PercentType(newBrightness);
+ } else {
+ try {
+ BigDecimal value = new BigDecimal(string);
+ if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+ value = PercentType.HUNDRED.toBigDecimal();
+ }
+ if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+ value = PercentType.ZERO.toBigDecimal();
+ }
+ newState = new PercentType(value);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ }
+
+ state = newState;
+ return Optional.of(newState);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java
new file mode 100644
index 0000000000..47ea726d3b
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link FixedValueMappingChannelHandler} implements mapping conversions for different item-types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class FixedValueMappingChannelHandler extends AbstractTransformingChannelHandler {
+
+ public FixedValueMappingChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String value = channelConfig.commandToFixedValue(command);
+ if (value != null) {
+ return value;
+ }
+
+ throw new IllegalArgumentException(
+ "Command type '" + command.toString() + "' not supported or mapping not defined.");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State state = channelConfig.fixedValueToState(string);
+
+ return Optional.of(Objects.requireNonNullElse(state, UnDefType.UNDEF));
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java
new file mode 100644
index 0000000000..aa25055270
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link GenericChannelHandler} implements simple conversions for different item types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GenericChannelHandler extends AbstractTransformingChannelHandler {
+ private final Function toState;
+
+ public GenericChannelHandler(Function toState, Consumer updateState,
+ Consumer postCommand, @Nullable Consumer sendValue,
+ ChannelTransformation stateTransformations, ChannelTransformation commandTransformations,
+ ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ this.toState = toState;
+ }
+
+ protected Optional toState(String value) {
+ try {
+ return Optional.of(toState.apply(value));
+ } catch (IllegalArgumentException e) {
+ return Optional.of(UnDefType.UNDEF);
+ }
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ protected String toString(Command command) {
+ return command.toString();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java
new file mode 100644
index 0000000000..ff23ea3289
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.thing.binding.generic.ChannelHandler;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ImageChannelHandler} implements {@link org.openhab.core.library.items.ImageItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ImageChannelHandler implements ChannelHandler {
+ private final Consumer updateState;
+
+ public ImageChannelHandler(Consumer updateState) {
+ this.updateState = updateState;
+ }
+
+ @Override
+ public void process(@Nullable ChannelHandlerContent content) {
+ if (content == null) {
+ updateState.accept(UnDefType.UNDEF);
+ return;
+ }
+ String mediaType = content.getMediaType();
+ updateState.accept(
+ new RawType(content.getRawContent(), mediaType != null ? mediaType : RawType.DEFAULT_MIME_TYPE));
+ }
+
+ @Override
+ public void send(Command command) {
+ throw new IllegalStateException("Read-only channel");
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java
new file mode 100644
index 0000000000..91a48514d4
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import javax.measure.format.MeasurementParseException;
+
+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.QuantityType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link NumberChannelHandler} implements {@link org.openhab.core.library.items.NumberItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class NumberChannelHandler extends AbstractTransformingChannelHandler {
+
+ public NumberChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ protected Optional toState(String value) {
+ String trimmedValue = value.trim();
+ State newState = UnDefType.UNDEF;
+ if (!trimmedValue.isEmpty()) {
+ try {
+ if (channelConfig.unit != null) {
+ // we have a given unit - use that
+ newState = new QuantityType<>(trimmedValue + " " + channelConfig.unit);
+ } else {
+ try {
+ // try if we have a simple number
+ newState = new DecimalType(trimmedValue);
+ } catch (IllegalArgumentException e1) {
+ // not a plain number, maybe with unit?
+ newState = new QuantityType<>(trimmedValue);
+ }
+ }
+ } catch (IllegalArgumentException | MeasurementParseException e) {
+ // finally failed
+ }
+ }
+ return Optional.of(newState);
+ }
+
+ @Override
+ protected String toString(Command command) {
+ return command.toString();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java
new file mode 100644
index 0000000000..0afe6f41c1
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link PlayerChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class PlayerChannelHandler extends AbstractTransformingChannelHandler {
+ private @Nullable String lastCommand; // store last command to prevent duplicate commands
+
+ public PlayerChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String string) {
+ if (string.equals(lastCommand)) {
+ // only send commands once
+ return null;
+ }
+ lastCommand = string;
+
+ if (string.equals(channelConfig.playValue)) {
+ return PlayPauseType.PLAY;
+ } else if (string.equals(channelConfig.pauseValue)) {
+ return PlayPauseType.PAUSE;
+ } else if (string.equals(channelConfig.nextValue)) {
+ return NextPreviousType.NEXT;
+ } else if (string.equals(channelConfig.previousValue)) {
+ return NextPreviousType.PREVIOUS;
+ } else if (string.equals(channelConfig.rewindValue)) {
+ return RewindFastforwardType.REWIND;
+ } else if (string.equals(channelConfig.fastforwardValue)) {
+ return RewindFastforwardType.FASTFORWARD;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Optional toState(String string) {
+ // no value - we ignore state updates
+ return Optional.empty();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java
new file mode 100644
index 0000000000..cf0ef1df0a
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link RollershutterChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class RollershutterChannelHandler extends AbstractTransformingChannelHandler {
+
+ public RollershutterChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof PercentType) {
+ final String downValue = channelConfig.downValue;
+ final String upValue = channelConfig.upValue;
+ if (command.equals(PercentType.HUNDRED) && downValue != null) {
+ return downValue;
+ } else if (command.equals(PercentType.ZERO) && upValue != null) {
+ return upValue;
+ } else {
+ return ((PercentType) command).toString();
+ }
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String string) {
+ if (string.equals(channelConfig.upValue)) {
+ return UpDownType.UP;
+ } else if (string.equals(channelConfig.downValue)) {
+ return UpDownType.DOWN;
+ } else if (string.equals(channelConfig.moveValue)) {
+ return StopMoveType.MOVE;
+ } else if (string.equals(channelConfig.stopValue)) {
+ return StopMoveType.STOP;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+ try {
+ BigDecimal value = new BigDecimal(string);
+ if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+ value = PercentType.HUNDRED.toBigDecimal();
+ }
+ if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+ value = PercentType.ZERO.toBigDecimal();
+ }
+ newState = new PercentType(value);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+
+ return Optional.of(newState);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java
new file mode 100644
index 0000000000..b4e332f0c9
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.internal.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelHandler;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelMode;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link AbstractTransformingChannelHandler} is a base class for an item converter with transformations
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractTransformingChannelHandler implements ChannelHandler {
+ private final Consumer updateState;
+ private final Consumer postCommand;
+ private final @Nullable Consumer sendValue;
+ private final ChannelTransformation stateTransformations;
+ private final ChannelTransformation commandTransformations;
+
+ protected final ChannelValueConverterConfig channelConfig;
+
+ public AbstractTransformingChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ this.updateState = updateState;
+ this.postCommand = postCommand;
+ this.sendValue = sendValue;
+ this.stateTransformations = stateTransformations;
+ this.commandTransformations = commandTransformations;
+ this.channelConfig = channelConfig;
+ }
+
+ @Override
+ public void process(@Nullable ChannelHandlerContent content) {
+ if (content == null) {
+ updateState.accept(UnDefType.UNDEF);
+ return;
+ }
+ if (channelConfig.mode != ChannelMode.WRITEONLY) {
+ stateTransformations.apply(content.getAsString()).ifPresent(transformedValue -> {
+ Command command = toCommand(transformedValue);
+ if (command != null) {
+ postCommand.accept(command);
+ } else {
+ toState(transformedValue).ifPresent(updateState);
+ }
+ });
+ } else {
+ throw new IllegalStateException("Write-only channel");
+ }
+ }
+
+ @Override
+ public void send(Command command) {
+ Consumer sendHttpValue = this.sendValue;
+ if (sendHttpValue != null && channelConfig.mode != ChannelMode.READONLY) {
+ commandTransformations.apply(toString(command)).ifPresent(sendHttpValue);
+ } else {
+ throw new IllegalStateException("Read-only channel");
+ }
+ }
+
+ /**
+ * check if this converter received a value that needs to be sent as command
+ *
+ * @param value the value
+ * @return the command or null
+ */
+ protected abstract @Nullable Command toCommand(String value);
+
+ /**
+ * convert the received value to a state
+ *
+ * @param value the value
+ * @return the state that represents the value of UNDEF if conversion failed
+ */
+ protected abstract Optional toState(String value);
+
+ /**
+ * convert a command to a string
+ *
+ * @param command the command
+ * @return the string representation of the command
+ */
+ protected abstract String toString(Command command);
+
+ @FunctionalInterface
+ public interface Factory {
+ ChannelHandler create(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendHttpValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java
new file mode 100644
index 0000000000..d7847b6322
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.transform.TransformationException;
+import org.openhab.core.transform.TransformationHelper;
+import org.openhab.core.transform.TransformationService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * The {@link ChannelTransformationTest} contains tests for the {@link ChannelTransformation}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class ChannelTransformationTest {
+ private static final String T1_NAME = "TRANSFORM1";
+ private static final String T1_PATTERN = "T1Pattern";
+ private static final String T1_INPUT = "T1Input";
+ private static final String T1_RESULT = "T1Result";
+
+ private static final String T2_NAME = "TRANSFORM2";
+ private static final String T2_PATTERN = "T2Pattern";
+ private static final String T2_INPUT = T1_RESULT;
+ private static final String T2_RESULT = "T2Result";
+
+ private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
+ private @Mock @NonNullByDefault({}) TransformationService transformationService2Mock;
+
+ private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
+ private @Mock @NonNullByDefault({}) ServiceReference serviceRef1Mock;
+ private @Mock @NonNullByDefault({}) ServiceReference serviceRef2Mock;
+
+ private @NonNullByDefault({}) TransformationHelper transformationHelper;
+
+ @BeforeEach
+ public void init() throws TransformationException {
+ Mockito.when(transformationService1Mock.transform(eq(T1_PATTERN), eq(T1_INPUT)))
+ .thenAnswer(answer -> T1_RESULT);
+ Mockito.when(transformationService2Mock.transform(eq(T2_PATTERN), eq(T1_INPUT)))
+ .thenAnswer(answer -> T2_RESULT);
+ Mockito.when(transformationService2Mock.transform(eq(T2_PATTERN), eq(T2_INPUT)))
+ .thenAnswer(answer -> T2_RESULT);
+
+ Mockito.when(serviceRef1Mock.getProperty(any())).thenReturn("TRANSFORM1");
+ Mockito.when(serviceRef2Mock.getProperty(any())).thenReturn("TRANSFORM2");
+
+ Mockito.when(bundleContextMock.getService(serviceRef1Mock)).thenReturn(transformationService1Mock);
+ Mockito.when(bundleContextMock.getService(serviceRef2Mock)).thenReturn(transformationService2Mock);
+
+ transformationHelper = new TransformationHelper(bundleContextMock);
+ transformationHelper.setTransformationService(serviceRef1Mock);
+ transformationHelper.setTransformationService(serviceRef2Mock);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ transformationHelper.deactivate();
+ }
+
+ @Test
+ public void testMissingTransformation() {
+ String pattern = "TRANSFORM:pattern";
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testSingleTransformation() {
+ String pattern = T1_NAME + ":" + T1_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T1_RESULT, result);
+ }
+
+ @Test
+ public void testInvalidFirstTransformation() {
+ String pattern = T1_NAME + "X:" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testInvalidSecondTransformation() {
+ String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + "X:" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testDoubleTransformationWithoutSpaces() {
+ String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T2_RESULT, result);
+ }
+
+ @Test
+ public void testDoubleTransformationWithSpaces() {
+ String pattern = " " + T1_NAME + " : " + T1_PATTERN + " ∩ " + T2_NAME + " : " + T2_PATTERN + " ";
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T2_RESULT, result);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java
new file mode 100644
index 0000000000..1d5e2e6b24
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.binding.generic.converter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ConverterTest} is a test class for state converters
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class ConverterTest {
+
+ private @Mock @NonNullByDefault({}) Consumer sendValueMock;
+
+ private @Mock @NonNullByDefault({}) Consumer updateStateMock;
+
+ private @Mock @NonNullByDefault({}) Consumer postCommandMock;
+
+ @Test
+ public void numberItemConverter() {
+ NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig());
+
+ // without unit
+ Assertions.assertEquals(Optional.of(new DecimalType(1234)), converter.toState("1234"));
+
+ // unit in transformation result
+ Assertions.assertEquals(Optional.of(new QuantityType<>(100, SIUnits.CELSIUS)), converter.toState("100°C"));
+
+ // no valid value
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("W"));
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState(""));
+ }
+
+ @Test
+ public void numberItemConverterWithUnit() {
+ ChannelValueConverterConfig channelConfig = new ChannelValueConverterConfig();
+ channelConfig.unit = "W";
+ NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), channelConfig);
+
+ // without unit
+ Assertions.assertEquals(Optional.of(new QuantityType<>(500, Units.WATT)), converter.toState("500"));
+
+ // no valid value
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("foo"));
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState(""));
+ }
+
+ @Test
+ public void stringTypeConverter() {
+ GenericChannelHandler converter = createConverter(StringType::new);
+ Assertions.assertEquals(Optional.of(new StringType("Test")), converter.toState("Test"));
+ }
+
+ @Test
+ public void decimalTypeConverter() {
+ GenericChannelHandler converter = createConverter(DecimalType::new);
+ Assertions.assertEquals(Optional.of(new DecimalType(15.6)), converter.toState("15.6"));
+ }
+
+ @Test
+ public void pointTypeConverter() {
+ GenericChannelHandler converter = createConverter(PointType::new);
+ Assertions.assertEquals(
+ Optional.of(new PointType(new DecimalType(51.1), new DecimalType(7.2), new DecimalType(100))),
+ converter.toState("51.1, 7.2, 100"));
+ }
+
+ @Test
+ public void playerItemTypeConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.playValue = "PLAY";
+ ChannelHandlerContent content = new ChannelHandlerContent("PLAY".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ PlayerChannelHandler converter = new PlayerChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+ converter.process(content);
+ converter.process(content);
+
+ Mockito.verify(postCommandMock).accept(PlayPauseType.PLAY);
+ Mockito.verify(updateStateMock, Mockito.never()).accept(ArgumentMatchers.any());
+ }
+
+ @Test
+ public void colorItemTypeRGBConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.colorMode = ColorChannelHandler.ColorMode.RGB;
+ ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(HSBType.fromRGB(123, 34, 47));
+ }
+
+ @Test
+ public void colorItemTypeHSBConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.colorMode = ColorChannelHandler.ColorMode.HSB;
+ ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(new HSBType("123,34,47"));
+ }
+
+ @Test
+ public void rollerSHutterConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ RollershutterChannelHandler converter = new RollershutterChannelHandler(updateStateMock, postCommandMock,
+ sendValueMock, new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ // test 0 and 100
+ ChannelHandlerContent content = new ChannelHandlerContent("0".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(PercentType.ZERO);
+ content = new ChannelHandlerContent("100".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(PercentType.HUNDRED);
+
+ // test under/over-range (expect two times total for zero/100
+ content = new ChannelHandlerContent("-1".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.ZERO);
+ content = new ChannelHandlerContent("105".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.HUNDRED);
+
+ // test value
+ content = new ChannelHandlerContent("67".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(new PercentType(67));
+ }
+
+ public GenericChannelHandler createConverter(Function fcn) {
+ return new GenericChannelHandler(fcn, updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig());
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java
new file mode 100644
index 0000000000..08207319d4
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2023 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.core.thing.internal.binding.generic.converter;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link AbstractTransformingItemConverterTest} is a test class for the
+ * {@link AbstractTransformingChannelHandler}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class AbstractTransformingItemConverterTest {
+
+ @Mock
+ private @NonNullByDefault({}) Consumer sendHttpValue;
+
+ @Mock
+ private @NonNullByDefault({}) Consumer updateState;
+
+ @Mock
+ private @NonNullByDefault({}) Consumer postCommand;
+
+ private @NonNullByDefault({}) AutoCloseable closeable;
+
+ @Spy
+ private ChannelTransformation stateChannelTransformation = new ChannelTransformation(null);
+
+ @Spy
+ private ChannelTransformation commandChannelTransformation = new ChannelTransformation(null);
+
+ @BeforeEach
+ public void init() {
+ closeable = MockitoAnnotations.openMocks(this);
+ }
+
+ @AfterEach
+ public void close() throws Exception {
+ closeable.close();
+ }
+
+ @Test
+ public void undefOnNullContentTest() {
+ TestChannelHandler realConverter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+ TestChannelHandler converter = spy(realConverter);
+
+ converter.process(null);
+ // make sure UNDEF is send as state update
+ verify(updateState, only()).accept(UnDefType.UNDEF);
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, never()).accept(any());
+
+ // make sure no other processing applies
+ verify(converter, never()).toState(any());
+ verify(converter, never()).toCommand(any());
+ verify(converter, never()).toString(any());
+ }
+
+ @Test
+ public void commandIsPostedAsCommand() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, true);
+
+ converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null));
+
+ // check state transformation is applied
+ verify(stateChannelTransformation).apply(any());
+ verify(commandChannelTransformation, never()).apply(any());
+
+ // check only postCommand is applied
+ verify(updateState, never()).accept(any());
+ verify(postCommand, only()).accept(new StringType("TEST"));
+ verify(sendHttpValue, never()).accept(any());
+ }
+
+ @Test
+ public void updateIsPostedAsUpdate() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+
+ converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null));
+
+ // check state transformation is applied
+ verify(stateChannelTransformation).apply(any());
+ verify(commandChannelTransformation, never()).apply(any());
+
+ // check only updateState is called
+ verify(updateState, only()).accept(new StringType("TEST"));
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, never()).accept(any());
+ }
+
+ @Test
+ public void sendCommandSendsCommand() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+
+ converter.send(new StringType("TEST"));
+
+ // check command transformation is applied
+ verify(stateChannelTransformation, never()).apply(any());
+ verify(commandChannelTransformation).apply(any());
+
+ // check only sendHttpValue is applied
+ verify(updateState, never()).accept(any());
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, only()).accept("TEST");
+ }
+
+ private static class TestChannelHandler extends AbstractTransformingChannelHandler {
+ private boolean hasCommand;
+
+ public TestChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateChannelTransformation,
+ ChannelTransformation commandChannelTransformation, boolean hasCommand) {
+ super(updateState, postCommand, sendValue, stateChannelTransformation, commandChannelTransformation,
+ new ChannelValueConverterConfig());
+ this.hasCommand = hasCommand;
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return hasCommand ? new StringType(value) : null;
+ }
+
+ @Override
+ protected Optional toState(String value) {
+ return Optional.of(new StringType(value));
+ }
+
+ @Override
+ protected String toString(Command command) {
+ return command.toString();
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
index 93500be5e1..94e953d725 100644
--- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
+++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
@@ -12,25 +12,34 @@
*/
package org.openhab.core.transform;
-import java.util.Collection;
import java.util.IllegalFormatException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.framework.BundleContext;
-import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Kai Kreuzer - Initial contribution
+ * @author Jan N. Klug - Refactored to OSGi service
*/
+@Component(immediate = true)
@NonNullByDefault
public class TransformationHelper {
+ private static final Map SERVICES = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(TransformationHelper.class);
@@ -40,6 +49,35 @@ public class TransformationHelper {
protected static final Pattern EXTRACT_TRANSFORMFUNCTION_PATTERN = Pattern
.compile("(.*?)\\((.*)\\)" + FUNCTION_VALUE_DELIMITER + "(.*)");
+ private final BundleContext bundleContext;
+
+ @Activate
+ public TransformationHelper(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ @Deactivate
+ public void deactivate() {
+ SERVICES.clear();
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void setTransformationService(ServiceReference ref) {
+ String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME);
+ TransformationService service = bundleContext.getService(ref);
+ if (service != null) {
+ SERVICES.put(key, service);
+ LOGGER.debug("Added transformation service {}", key);
+ }
+ }
+
+ public void unsetTransformationService(ServiceReference ref) {
+ String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME);
+ if (SERVICES.remove(key) != null) {
+ LOGGER.debug("Removed transformation service {}", key);
+ }
+ }
+
/**
* determines whether a pattern refers to a transformation service
*
@@ -50,52 +88,57 @@ public class TransformationHelper {
return EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(pattern).matches();
}
+ public static @Nullable TransformationService getTransformationService(String serviceName) {
+ return SERVICES.get(serviceName);
+ }
+
/**
- * Queries the OSGi service registry for a service that provides a transformation service of
- * a given transformation type (e.g. REGEX, XSLT, etc.)
+ * Return the transformation service that provides a given transformation type (e.g. REGEX, XSLT, etc.)
*
* @param context the bundle context which can be null
* @param transformationType the desired transformation type
* @return a service instance or null, if none could be found
+ *
+ * @deprecated use {@link #getTransformationService(String)} instead
*/
+ @Deprecated
public static @Nullable TransformationService getTransformationService(@Nullable BundleContext context,
String transformationType) {
- if (context != null) {
- String filter = "(" + TransformationService.SERVICE_PROPERTY_NAME + "=" + transformationType + ")";
- try {
- Collection> refs = context
- .getServiceReferences(TransformationService.class, filter);
- if (refs != null && !refs.isEmpty()) {
- return context.getService(refs.iterator().next());
- } else {
- LOGGER.debug("Cannot get service reference for transformation service of type {}",
- transformationType);
- }
- } catch (InvalidSyntaxException e) {
- LOGGER.debug("Cannot get service reference for transformation service of type {}", transformationType,
- e);
- }
- }
- return null;
+ return getTransformationService(transformationType);
}
/**
* Transforms a state string using transformation functions within a given pattern.
*
* @param context a valid bundle context, required for accessing the services
- * @param stateDescPattern the pattern that contains the transformation instructions
+ * @param transformationString the pattern that contains the transformation instructions
+ * @param state the state to be formatted before being passed into the transformation function
+ * @return the result of the transformation. If no transformation was done, null
is returned
+ * @throws TransformationException if transformation service is not available or the transformation failed
+ *
+ * @deprecated Use {@link #transform(String, String)} instead
+ */
+ @Deprecated
+ public static @Nullable String transform(BundleContext context, String transformationString, String state)
+ throws TransformationException {
+ return transform(transformationString, state);
+ }
+
+ /**
+ * Transforms a state string using transformation functions within a given pattern.
+ *
+ * @param transformationString the pattern that contains the transformation instructions
* @param state the state to be formatted before being passed into the transformation function
* @return the result of the transformation. If no transformation was done, null
is returned
* @throws TransformationException if transformation service is not available or the transformation failed
*/
- public static @Nullable String transform(BundleContext context, String stateDescPattern, String state)
- throws TransformationException {
- Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(stateDescPattern);
+ public static @Nullable String transform(String transformationString, String state) throws TransformationException {
+ Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(transformationString);
if (matcher.find()) {
String type = matcher.group(1);
String pattern = matcher.group(2);
String value = matcher.group(3);
- TransformationService transformation = TransformationHelper.getTransformationService(context, type);
+ TransformationService transformation = SERVICES.get(type);
if (transformation != null) {
return transform(transformation, pattern, value, state);
} else {
diff --git a/itests/org.openhab.core.automation.integration.tests/itest.bndrun b/itests/org.openhab.core.automation.integration.tests/itest.bndrun
index 21aafaab4e..9323415d07 100644
--- a/itests/org.openhab.core.automation.integration.tests/itest.bndrun
+++ b/itests/org.openhab.core.automation.integration.tests/itest.bndrun
@@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.automation
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
org.objenesis;version='[3.3.0,3.3.1)',\
- org.osgi.service.cm;version='[1.6.0,1.6.1)'
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'
diff --git a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun
index de798e868f..f87b5a7100 100644
--- a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun
+++ b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun
@@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.automation
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
org.objenesis;version='[3.3.0,3.3.1)',\
- org.osgi.service.cm;version='[1.6.0,1.6.1)'
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'
diff --git a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun
index 364e3f2dca..5e6f415c49 100644
--- a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun
+++ b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun
@@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.automation
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
org.objenesis;version='[3.3.0,3.3.1)',\
- org.osgi.service.cm;version='[1.6.0,1.6.1)'
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'
diff --git a/itests/org.openhab.core.automation.tests/itest.bndrun b/itests/org.openhab.core.automation.tests/itest.bndrun
index 55ed88082d..98fb146d9a 100644
--- a/itests/org.openhab.core.automation.tests/itest.bndrun
+++ b/itests/org.openhab.core.automation.tests/itest.bndrun
@@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.automation
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
org.objenesis;version='[3.3.0,3.3.1)',\
- org.osgi.service.cm;version='[1.6.0,1.6.1)'
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'
diff --git a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun
index 1c4f83e761..b283f7cac4 100644
--- a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun
+++ b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun
@@ -65,4 +65,6 @@ Fragment-Host: org.openhab.core.config.discovery.mdns
net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
- org.objenesis;version='[3.3.0,3.3.1)'
+ org.objenesis;version='[3.3.0,3.3.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)'
diff --git a/itests/org.openhab.core.config.discovery.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.tests/itest.bndrun
index 2f70c26108..ad2ab040a7 100644
--- a/itests/org.openhab.core.config.discovery.tests/itest.bndrun
+++ b/itests/org.openhab.core.config.discovery.tests/itest.bndrun
@@ -64,4 +64,6 @@ Fragment-Host: org.openhab.core.config.discovery
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
- org.objenesis;version='[3.3.0,3.3.1)'
+ org.objenesis;version='[3.3.0,3.3.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)'
diff --git a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun
index 402acf7ea7..5f27c62228 100644
--- a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun
+++ b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun
@@ -65,4 +65,6 @@ Fragment-Host: org.openhab.core.config.discovery.usbserial.linuxsysfs
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
- org.objenesis;version='[3.3.0,3.3.1)'
+ org.objenesis;version='[3.3.0,3.3.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)'
diff --git a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun
index 956ee342b1..20edb85c9d 100644
--- a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun
+++ b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun
@@ -73,4 +73,6 @@ Provide-Capability: \
net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
- org.objenesis;version='[3.3.0,3.3.1)'
+ org.objenesis;version='[3.3.0,3.3.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)'
diff --git a/itests/org.openhab.core.storage.json.tests/itest.bndrun b/itests/org.openhab.core.storage.json.tests/itest.bndrun
index 1f52ea404a..d029def728 100644
--- a/itests/org.openhab.core.storage.json.tests/itest.bndrun
+++ b/itests/org.openhab.core.storage.json.tests/itest.bndrun
@@ -58,4 +58,6 @@ Fragment-Host: org.openhab.core.storage.json
junit-jupiter-engine;version='[5.9.2,5.9.3)',\
junit-platform-commons;version='[1.9.2,1.9.3)',\
junit-platform-engine;version='[1.9.2,1.9.3)',\
- junit-platform-launcher;version='[1.9.2,1.9.3)'
+ junit-platform-launcher;version='[1.9.2,1.9.3)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)'
diff --git a/itests/org.openhab.core.thing.tests/itest.bndrun b/itests/org.openhab.core.thing.tests/itest.bndrun
index 93bb6e0023..dc229ee0af 100644
--- a/itests/org.openhab.core.thing.tests/itest.bndrun
+++ b/itests/org.openhab.core.thing.tests/itest.bndrun
@@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.thing
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
- org.objenesis;version='[3.3.0,3.3.1)'
+ org.objenesis;version='[3.3.0,3.3.1)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'
diff --git a/itests/org.openhab.core.voice.tests/itest.bndrun b/itests/org.openhab.core.voice.tests/itest.bndrun
index b4e85f066f..5d0738bd7d 100644
--- a/itests/org.openhab.core.voice.tests/itest.bndrun
+++ b/itests/org.openhab.core.voice.tests/itest.bndrun
@@ -72,4 +72,5 @@ Fragment-Host: org.openhab.core.voice
junit-jupiter-params;version='[5.9.2,5.9.3)',\
junit-platform-commons;version='[1.9.2,1.9.3)',\
junit-platform-engine;version='[1.9.2,1.9.3)',\
- junit-platform-launcher;version='[1.9.2,1.9.3)'
+ junit-platform-launcher;version='[1.9.2,1.9.3)',\
+ org.openhab.core.transform;version='[4.0.0,4.0.1)'