Add support for things with generic channels (#3355)

* Add support for generic channels

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3674/head
J-N-K 2023-06-25 16:22:55 +02:00 committed by GitHub
parent 64fd046266
commit 78e66745ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1776 additions and 37 deletions

View File

@ -25,6 +25,11 @@
<artifactId>org.openhab.core.io.console</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.transform</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>

View File

@ -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 (<code>null</code> 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);
}

View File

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

View File

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

View File

@ -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 <code></code> and must follow the pattern
* <code>serviceName:function</code> where <code>serviceName</code> refers to a {@link TransformationService} and
* <code>function</code> 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<TransformationStep> 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<String> apply(String value) {
Optional<String> 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<String> 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 + "'}";
}
}
}

View File

@ -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<String, State> stringStateMap = new HashMap<>();
private final Map<Command, @Nullable String> 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);
}
}
}

View File

@ -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("(?<r>\\d+),(?<g>\\d+),(?<b>\\d+)");
private State state = UnDefType.UNDEF;
public ColorChannelHandler(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> 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
}
}

View File

@ -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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> 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);
}
}

View File

@ -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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> toState(String string) {
State state = channelConfig.fixedValueToState(string);
return Optional.of(Objects.requireNonNullElse(state, UnDefType.UNDEF));
}
}

View File

@ -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<String, State> toState;
public GenericChannelHandler(Function<String, State> toState, Consumer<State> updateState,
Consumer<Command> postCommand, @Nullable Consumer<String> sendValue,
ChannelTransformation stateTransformations, ChannelTransformation commandTransformations,
ChannelValueConverterConfig channelConfig) {
super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
this.toState = toState;
}
protected Optional<State> 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();
}
}

View File

@ -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<State> updateState;
public ImageChannelHandler(Consumer<State> 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");
}
}

View File

@ -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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> 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();
}
}

View File

@ -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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> toState(String string) {
// no value - we ignore state updates
return Optional.empty();
}
}

View File

@ -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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> 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);
}
}

View File

@ -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<State> updateState;
private final Consumer<Command> postCommand;
private final @Nullable Consumer<String> sendValue;
private final ChannelTransformation stateTransformations;
private final ChannelTransformation commandTransformations;
protected final ChannelValueConverterConfig channelConfig;
public AbstractTransformingChannelHandler(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<String> 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<State> 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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ChannelTransformation stateTransformations,
ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig);
}
}

View File

@ -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<TransformationService> serviceRef1Mock;
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> 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);
}
}

View File

@ -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<String> sendValueMock;
private @Mock @NonNullByDefault({}) Consumer<State> updateStateMock;
private @Mock @NonNullByDefault({}) Consumer<Command> 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<String, State> fcn) {
return new GenericChannelHandler(fcn, updateStateMock, postCommandMock, sendValueMock,
new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig());
}
}

View File

@ -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<String> sendHttpValue;
@Mock
private @NonNullByDefault({}) Consumer<State> updateState;
@Mock
private @NonNullByDefault({}) Consumer<Command> 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<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> 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<State> toState(String value) {
return Optional.of(new StringType(value));
}
@Override
protected String toString(Command command) {
return command.toString();
}
}
}

View File

@ -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<String, TransformationService> 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<TransformationService> 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<TransformationService> 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<ServiceReference<TransformationService>> 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, <code>null</code> 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, <code>null</code> 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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