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)'