From 9c0302ec26d2c37e1519f449f4b73b4a961bfd21 Mon Sep 17 00:00:00 2001 From: Christoph Weitkamp Date: Sat, 27 Feb 2021 10:28:09 +0100 Subject: [PATCH] [profiles] Added 'Range' profile (#2046) Signed-off-by: Christoph Weitkamp --- .../SystemHysteresisStateProfile.java | 2 +- .../profiles/SystemProfileFactory.java | 12 +- .../profiles/SystemRangeStateProfile.java | 174 +++++++++++++ .../core/thing/profiles/SystemProfiles.java | 7 + .../resources/OH-INF/config/rangeProfile.xml | 22 ++ .../OH-INF/i18n/SystemProfiles_de.properties | 15 +- .../profiles/SystemRangeStateProfileTest.java | 244 ++++++++++++++++++ .../SystemProfileFactoryOSGiTest.java | 5 +- 8 files changed, 469 insertions(+), 12 deletions(-) create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java create mode 100644 bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/rangeProfile.xml create mode 100644 bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java index 3314f7a8e..9177deb5d 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import tec.uom.se.AbstractUnit; /*** - * This is the default implementation for a {@link SystemHysteresisStateProfile}}. + * This is the default implementation for a {@link SystemHysteresisStateProfile}. * * @author Christoph Weitkamp - Initial contribution */ diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactory.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactory.java index d3702dba2..958194b8a 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactory.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactory.java @@ -64,13 +64,13 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro private final ChannelTypeRegistry channelTypeRegistry; private static final Set SUPPORTED_PROFILE_TYPES = Set.of(DEFAULT_TYPE, FOLLOW_TYPE, HYSTERESIS_TYPE, - OFFSET_TYPE, RAWBUTTON_ON_OFF_SWITCH_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE, RAWBUTTON_TOGGLE_SWITCH_TYPE, - RAWROCKER_DIMMER_TYPE, RAWROCKER_NEXT_PREVIOUS_TYPE, RAWROCKER_ON_OFF_TYPE, RAWROCKER_PLAY_PAUSE_TYPE, - RAWROCKER_REWIND_FASTFORWARD_TYPE, RAWROCKER_STOP_MOVE_TYPE, RAWROCKER_UP_DOWN_TYPE, TIMESTAMP_CHANGE_TYPE, - TIMESTAMP_UPDATE_TYPE); + OFFSET_TYPE, RANGE_TYPE, RAWBUTTON_ON_OFF_SWITCH_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE, + RAWBUTTON_TOGGLE_SWITCH_TYPE, RAWROCKER_DIMMER_TYPE, RAWROCKER_NEXT_PREVIOUS_TYPE, RAWROCKER_ON_OFF_TYPE, + RAWROCKER_PLAY_PAUSE_TYPE, RAWROCKER_REWIND_FASTFORWARD_TYPE, RAWROCKER_STOP_MOVE_TYPE, + RAWROCKER_UP_DOWN_TYPE, TIMESTAMP_CHANGE_TYPE, TIMESTAMP_UPDATE_TYPE); private static final Set SUPPORTED_PROFILE_TYPE_UIDS = Set.of(DEFAULT, FOLLOW, HYSTERESIS, OFFSET, - RAWBUTTON_ON_OFF_SWITCH, RAWBUTTON_TOGGLE_PLAYER, RAWBUTTON_TOGGLE_SWITCH, RAWROCKER_DIMMER, + RANGE, RAWBUTTON_ON_OFF_SWITCH, RAWBUTTON_TOGGLE_PLAYER, RAWBUTTON_TOGGLE_SWITCH, RAWROCKER_DIMMER, RAWROCKER_NEXT_PREVIOUS, RAWROCKER_ON_OFF, RAWROCKER_PLAY_PAUSE, RAWROCKER_REWIND_FASTFORWARD, RAWROCKER_STOP_MOVE, RAWROCKER_UP_DOWN, TIMESTAMP_CHANGE, TIMESTAMP_UPDATE); @@ -99,6 +99,8 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro return new SystemHysteresisStateProfile(callback, context); } else if (OFFSET.equals(profileTypeUID)) { return new SystemOffsetProfile(callback, context); + } else if (RANGE.equals(profileTypeUID)) { + return new SystemRangeStateProfile(callback, context); } else if (RAWBUTTON_ON_OFF_SWITCH.equals(profileTypeUID)) { return new RawButtonOnOffSwitchProfile(callback); } else if (RAWBUTTON_TOGGLE_SWITCH.equals(profileTypeUID)) { diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java new file mode 100644 index 000000000..b3d05a78f --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.profiles; + +import java.math.BigDecimal; + +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.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.thing.profiles.StateProfile; +import org.openhab.core.thing.profiles.SystemProfiles; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.Type; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tec.uom.se.AbstractUnit; + +/*** + * This is the default implementation for a {@link SystemRangeStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class SystemRangeStateProfile implements StateProfile { + + static final String LOWER_PARAM = "lower"; + static final String UPPER_PARAM = "upper"; + static final String INVERTED_PARAM = "inverted"; + + private final Logger logger = LoggerFactory.getLogger(SystemRangeStateProfile.class); + + private final ProfileCallback callback; + + private final QuantityType lower; + private final QuantityType upper; + private final OnOffType inRange; + private final OnOffType notInRange; + + private Type previousType = UnDefType.UNDEF; + + public SystemRangeStateProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + + final QuantityType lowerParam = getParam(context, LOWER_PARAM); + if (lowerParam == null) { + throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", LOWER_PARAM)); + } + this.lower = lowerParam; + final QuantityType upperParam = getParam(context, UPPER_PARAM); + if (upperParam == null) { + throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", UPPER_PARAM)); + } + final QuantityType convertedUpperParam = upperParam.toUnit(lower.getUnit()); + if (convertedUpperParam == null) { + throw new IllegalArgumentException( + String.format("Units of parameters '%s' and '%s' are not compatible: %s != %s", LOWER_PARAM, + UPPER_PARAM, lower, upperParam)); + } + if (convertedUpperParam.doubleValue() <= lower.doubleValue()) { + throw new IllegalArgumentException( + String.format("Parameter '%s' (%s) is less than or equal to '%s' (%s) parameter.", UPPER_PARAM, + convertedUpperParam, LOWER_PARAM, lower)); + } + this.upper = convertedUpperParam; + + final Object paramValue = context.getConfiguration().get(INVERTED_PARAM); + final boolean inverted = paramValue == null ? false : Boolean.valueOf(paramValue.toString()); + this.inRange = inverted ? OnOffType.OFF : OnOffType.ON; + this.notInRange = inverted ? OnOffType.ON : OnOffType.OFF; + } + + private @Nullable QuantityType getParam(ProfileContext context, String param) { + final Object paramValue = context.getConfiguration().get(param); + logger.debug("Configuring profile with {} parameter '{}'", param, paramValue); + if (paramValue instanceof String) { + try { + return new QuantityType<>((String) paramValue); + } catch (IllegalArgumentException e) { + logger.error("Cannot convert value '{}' of parameter {} into a valid QuantityType.", paramValue, param); + } + } else if (paramValue instanceof BigDecimal) { + final BigDecimal value = (BigDecimal) paramValue; + return QuantityType.valueOf(value.doubleValue(), AbstractUnit.ONE); + } + return null; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return SystemProfiles.RANGE; + } + + @Override + public void onStateUpdateFromItem(State state) { + // do nothing + } + + @Override + public void onCommandFromHandler(Command command) { + final Type mappedCommand = mapValue(command); + logger.trace("Mapped command from '{}' to command '{}'.", command, mappedCommand); + if (mappedCommand instanceof Command) { + callback.sendCommand((Command) mappedCommand); + } + } + + @Override + public void onCommandFromItem(Command command) { + // do nothing + } + + @Override + public void onStateUpdateFromHandler(State state) { + final Type mappedState = mapValue(state); + logger.trace("Mapped state from '{}' to state '{}'.", state, mappedState); + if (mappedState instanceof State) { + callback.sendUpdate((State) mappedState); + } + } + + private Type mapValue(Type value) { + if (value instanceof QuantityType) { + final QuantityType qtState = (QuantityType) value; + final QuantityType finalLower; + final QuantityType finalUpper; + if (lower.getUnit() == Units.ONE && upper.getUnit() == Units.ONE) { + // allow bounds without unit -> implicitly assume its the same as the one from the state, but warn + // the user + finalLower = new QuantityType<>(lower.toBigDecimal(), qtState.getUnit()); + finalUpper = new QuantityType<>(upper.toBigDecimal(), qtState.getUnit()); + logger.warn( + "Received a QuantityType '{}' with unit, but the boundaries are defined as a plain number without units (lower={}, upper={}), please consider adding units to them.", + value, lower, upper); + } else { + finalLower = lower.toUnit(qtState.getUnit()); + finalUpper = upper.toUnit(qtState.getUnit()); + if (finalLower == null || finalUpper == null) { + logger.warn( + "Cannot compare state '{}' to boundaries because units (lower={}, upper={}) do not match.", + qtState, lower, upper); + return previousType; + } + } + return previousType = mapValue(finalLower.doubleValue(), finalUpper.doubleValue(), qtState.doubleValue()); + } else if (value instanceof DecimalType) { + return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), + ((DecimalType) value).doubleValue()); + } + return previousType; + } + + private Type mapValue(double lower, double upper, double value) { + return lower <= value && value <= upper ? inRange : notInRange; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/SystemProfiles.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/SystemProfiles.java index 3258a003a..9b10d8763 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/SystemProfiles.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/SystemProfiles.java @@ -30,6 +30,7 @@ public interface SystemProfiles { ProfileTypeUID FOLLOW = new ProfileTypeUID(SYSTEM_SCOPE, "follow"); ProfileTypeUID OFFSET = new ProfileTypeUID(SYSTEM_SCOPE, "offset"); ProfileTypeUID HYSTERESIS = new ProfileTypeUID(SYSTEM_SCOPE, "hysteresis"); + ProfileTypeUID RANGE = new ProfileTypeUID(SYSTEM_SCOPE, "range"); ProfileTypeUID RAWBUTTON_ON_OFF_SWITCH = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-on-off-switch"); ProfileTypeUID RAWBUTTON_TOGGLE_PLAYER = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-toggle-player"); ProfileTypeUID RAWBUTTON_TOGGLE_ROLLERSHUTTER = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-toggle-rollershutter"); @@ -51,11 +52,17 @@ public interface SystemProfiles { StateProfileType OFFSET_TYPE = ProfileTypeBuilder.newState(OFFSET, "Offset") .withSupportedItemTypes(CoreItemFactory.NUMBER).withSupportedItemTypesOfChannel(CoreItemFactory.NUMBER) .build(); + ProfileType HYSTERESIS_TYPE = ProfileTypeBuilder.newState(HYSTERESIS, "Hysteresis") // .withSupportedItemTypesOfChannel(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER) // .withSupportedItemTypes(CoreItemFactory.SWITCH) // .build(); + ProfileType RANGE_TYPE = ProfileTypeBuilder.newState(RANGE, "Range") // + .withSupportedItemTypesOfChannel(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER) // + .withSupportedItemTypes(CoreItemFactory.SWITCH) // + .build(); + TriggerProfileType RAWBUTTON_ON_OFF_SWITCH_TYPE = ProfileTypeBuilder .newTrigger(RAWBUTTON_ON_OFF_SWITCH, "Raw Button To On Off") .withSupportedItemTypes(CoreItemFactory.SWITCH, CoreItemFactory.DIMMER, CoreItemFactory.COLOR) diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/rangeProfile.xml b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/rangeProfile.xml new file mode 100644 index 000000000..487996081 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/rangeProfile.xml @@ -0,0 +1,22 @@ + + + + + + + Maps to ON if value is between lower and upper bound (plain number or number with unit). + + + + Maps to ON if value is between lower and upper bound (plain number or number with unit). + + + + Inverts resulting mapping of ON / OFF, if true. + false + + + diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties index e4b991f0f..9e23d12e4 100644 --- a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties @@ -2,13 +2,20 @@ profile-type.system.default.label = Standard profile-type.system.follow.label = Folgen profile-type.system.offset.label = Versatz profile.config.system.offset.offset.label = Versatz -profile.config.system.offset.offset.description = Versatz (Numerischer Wert oder numerischer Wert mit Einheit), welcher auf den Wert addiert wird. Ein negativer Versatz wird vom Wert abgezogen. +profile.config.system.offset.offset.description = Versatz (numerischer Wert oder numerischer Wert mit Einheit), welcher auf den Wert addiert wird. Ein negativer Versatz wird vom Wert abgezogen. profile-type.system.hysteresis.label = Hysterese profile.config.system.hysteresis.lower.label = Untere Grenze -profile.config.system.hysteresis.lower.description = Bildet bei Unterschreitung des unteren Grenzwertes OFF auf ein Switch Item ab (Numerischer Wert oder numerischer Wert mit Einheit). +profile.config.system.hysteresis.lower.description = Bildet bei Unterschreitung des unteren Grenzwertes OFF auf ein Switch Item ab (numerischer Wert oder numerischer Wert mit Einheit). profile.config.system.hysteresis.upper.label = Obere Grenze -profile.config.system.hysteresis.upper.description = Bildet bei Überschreitung des oberen Grenzwertes ON auf ein Switch Item ab (Numerischer Wert oder numerischer Wert mit Einheit). +profile.config.system.hysteresis.upper.description = Bildet bei Überschreitung des oberen Grenzwertes ON auf ein Switch Item ab (numerischer Wert oder numerischer Wert mit Einheit). profile.config.system.hysteresis.inverted.label = Invertiert profile.config.system.hysteresis.inverted.description = Invertiert die resultierende Zuordnung von ON / OFF, falls true. -profile-type.system.timestamp-change.label = Zeitstempel bei Änderung +profile-type.system.range.label = Bereich +profile.config.system.range.lower.label = Untere Grenze +profile.config.system.range.lower.description = Bildet bei ON auf ein Switch Item ab, wenn der Wert zwischen der unteren und oberen Grenze liegt (numerischer Wert oder numerischer Wert mit Einheit). +profile.config.system.range.upper.label = Obere Grenze +profile.config.system.range.upper.description = Bildet bei ON auf ein Switch Item ab, wenn der Wert zwischen der unteren und oberen Grenze liegt (numerischer Wert oder numerischer Wert mit Einheit). +profile.config.system.range.inverted.label = Invertiert +profile.config.system.range.inverted.description = Invertiert die resultierende Zuordnung von ON / OFF, falls true. +profile-type.system.timestamp-change.label = Zeitstempel bei Änderung profile-type.system.timestamp-update.label = Zeitstempel bei Aktualisierung diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java new file mode 100644 index 000000000..5aa66541d --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.profiles; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.StateProfile; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.Type; +import org.openhab.core.types.UnDefType; + +/** + * Basic unit tests for {@link SystemRangeStateProfileTest}. + * + * @author Christoph Weitkamp - Initial contribution + */ +public class SystemRangeStateProfileTest { + + private static final String STRING_FOURTY = "40"; + private static final String QUANTITY_STRING_TEN = "10 %"; + private static final String QUANTITY_STRING_FOURTY = "40 %"; + private static final BigDecimal BIGDECIMAL_FOURTY = new BigDecimal(STRING_FOURTY); + private static final PercentType PERCENT_TYPE_TEN = new PercentType(BigDecimal.TEN); + private static final PercentType PERCENT_TYPE_TWENTY_FIVE = new PercentType(BigDecimal.valueOf(25)); + + @NonNullByDefault + public static class ParameterSet { + public final List states; + public final List resultingStates; + public final List commands; + public final List<@Nullable Command> resultingCommands; + public final Object lower; + public final @Nullable Object upper; + public final boolean inverted; + + public ParameterSet(List sources, List results, Object lower, + @Nullable Object upper, boolean inverted) { + this.states = (List) sources; + this.resultingStates = (List) results; + this.commands = (List) sources; + this.resultingCommands = new ArrayList<>(results.size()); + results.forEach(result -> { + resultingCommands.add(result instanceof Command ? (Command) result : null); + }); + this.lower = lower; + this.upper = upper; + this.inverted = inverted; + } + } + + public static Collection parameters() { + return Arrays.asList(new Object[][] { // + // lower bound = 10, upper bound = 40 (as BigDecimal), one state update / command (PercentType), not + // inverted + { new ParameterSet(List.of(PercentType.HUNDRED), List.of(OnOffType.OFF), BigDecimal.TEN, + BIGDECIMAL_FOURTY, false) }, // + { new ParameterSet(List.of(PERCENT_TYPE_TWENTY_FIVE), List.of(OnOffType.ON), BigDecimal.TEN, + BIGDECIMAL_FOURTY, false) }, // + { new ParameterSet(List.of(PERCENT_TYPE_TEN), List.of(OnOffType.ON), BigDecimal.TEN, BIGDECIMAL_FOURTY, + false) }, // + { new ParameterSet(List.of(PercentType.ZERO), List.of(OnOffType.OFF), BigDecimal.TEN, BIGDECIMAL_FOURTY, + false) }, // + // lower bound = 10, upper bound = 40 (as BigDecimal), one state update / command (QuantityType), not + // inverted + { new ParameterSet(List.of(QuantityType.valueOf("100 %")), List.of(OnOffType.OFF), BigDecimal.TEN, + BIGDECIMAL_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf("25 %")), List.of(OnOffType.ON), BigDecimal.TEN, + BIGDECIMAL_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf(QUANTITY_STRING_TEN)), List.of(OnOffType.ON), + BigDecimal.TEN, BIGDECIMAL_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf("0 %")), List.of(OnOffType.OFF), BigDecimal.TEN, + BIGDECIMAL_FOURTY, false) }, // + // lower bound = 10, upper bound = 40 (as QuantityType), one state update / command (QuantityType), not + // inverted + { new ParameterSet(List.of(QuantityType.valueOf("100 %")), List.of(OnOffType.OFF), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf("25 %")), List.of(OnOffType.ON), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf(QUANTITY_STRING_TEN)), List.of(OnOffType.ON), + QUANTITY_STRING_TEN, QUANTITY_STRING_FOURTY, false) }, // + { new ParameterSet(List.of(QuantityType.valueOf("0 %")), List.of(OnOffType.OFF), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, false) }, // + // lower bound = 10, upper bound = 40 (as QuantityType), one state update / command (QuantityType) -> + // values + // are converted to the same unit, not inverted + { new ParameterSet(List.of(QuantityType.valueOf("10 m")), List.of(OnOffType.OFF), "25 cm", "100cm", + false) }, // + // lower bound = upper bound = 10 (as QuantityType), one state update / command (QuantityType) -> + // incompatible units cannot be compared + { new ParameterSet(List.of(QuantityType.valueOf("10 m")), List.of(UnDefType.UNDEF), "25 °C", "30 °C", + false) }, // + // lower bound = 10, upper bound = 40 (as BigDecimal), one state update / command (PercentType), + // inverted + { new ParameterSet(List.of(PercentType.HUNDRED), List.of(OnOffType.ON), BigDecimal.TEN, + BIGDECIMAL_FOURTY, true) }, // + { new ParameterSet(List.of(PERCENT_TYPE_TWENTY_FIVE), List.of(OnOffType.OFF), BigDecimal.TEN, + BIGDECIMAL_FOURTY, true) }, // + { new ParameterSet(List.of(PERCENT_TYPE_TEN), List.of(OnOffType.OFF), BigDecimal.TEN, BIGDECIMAL_FOURTY, + true) }, // + { new ParameterSet(List.of(PercentType.ZERO), List.of(OnOffType.ON), BigDecimal.TEN, BIGDECIMAL_FOURTY, + true) }, // + // lower bound = 10, upper bound = 40 (as BigDecimal), one state update / command (QuantityType), + // inverted + { new ParameterSet(List.of(QuantityType.valueOf("100 %")), List.of(OnOffType.ON), BigDecimal.TEN, + BIGDECIMAL_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf("25 %")), List.of(OnOffType.OFF), BigDecimal.TEN, + BIGDECIMAL_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf(QUANTITY_STRING_TEN)), List.of(OnOffType.OFF), + BigDecimal.TEN, BIGDECIMAL_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf("0 %")), List.of(OnOffType.ON), BigDecimal.TEN, + BIGDECIMAL_FOURTY, true) }, // + // lower bound = 10, upper bound = 40 (as QuantityType), one state update / command (QuantityType), + // inverted + { new ParameterSet(List.of(QuantityType.valueOf("100 %")), List.of(OnOffType.ON), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf("25 %")), List.of(OnOffType.OFF), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf(QUANTITY_STRING_TEN)), List.of(OnOffType.OFF), + QUANTITY_STRING_TEN, QUANTITY_STRING_FOURTY, true) }, // + { new ParameterSet(List.of(QuantityType.valueOf("0 %")), List.of(OnOffType.ON), QUANTITY_STRING_TEN, + QUANTITY_STRING_FOURTY, true) }, // + }); + } + + private AutoCloseable mocksCloseable; + + private @Mock ProfileCallback mockCallback; + private @Mock ProfileContext mockContext; + + @BeforeEach + public void setup() { + mocksCloseable = openMocks(this); + } + + @AfterEach + public void afterEach() throws Exception { + mocksCloseable.close(); + } + + @Test + public void testWrongParameterLower() { + assertThrows(IllegalArgumentException.class, () -> initProfile(null, null)); + } + + @Test + public void testWrongParameterUpper() { + assertThrows(IllegalArgumentException.class, () -> initProfile(QUANTITY_STRING_TEN, null)); + } + + @Test + public void testWrongParameterUnits() { + assertThrows(IllegalArgumentException.class, () -> initProfile(QUANTITY_STRING_TEN, "5 °C")); + } + + @Test + public void testWrongParameterUpperLessThanOrEqualsToLower() { + assertThrows(IllegalArgumentException.class, () -> initProfile(QUANTITY_STRING_FOURTY, QUANTITY_STRING_TEN)); + assertThrows(IllegalArgumentException.class, () -> initProfile(QUANTITY_STRING_FOURTY, QUANTITY_STRING_FOURTY)); + } + + @ParameterizedTest + @MethodSource("parameters") + public void testOnCommandFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(parameterSet.lower, parameterSet.upper, parameterSet.inverted); + for (int i = 0; i < parameterSet.commands.size(); i++) { + verifySendCommand(profile, parameterSet.commands.get(i), parameterSet.resultingCommands.get(i)); + } + } + + @ParameterizedTest + @MethodSource("parameters") + public void testOnStateUpdateFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(parameterSet.lower, parameterSet.upper, parameterSet.inverted); + for (int i = 0; i < parameterSet.states.size(); i++) { + verifySendUpdate(profile, parameterSet.states.get(i), parameterSet.resultingStates.get(i)); + } + } + + private StateProfile initProfile(Object lower, Object upper) { + return initProfile(lower, upper, false); + } + + private StateProfile initProfile(Object lower, Object upper, boolean inverted) { + final Map properties = new HashMap<>(2); + properties.put(SystemRangeStateProfile.LOWER_PARAM, lower); + properties.put(SystemRangeStateProfile.UPPER_PARAM, upper); + properties.put(SystemRangeStateProfile.INVERTED_PARAM, inverted); + when(mockContext.getConfiguration()).thenReturn(new Configuration(properties)); + return new SystemRangeStateProfile(mockCallback, mockContext); + } + + private void verifySendCommand(StateProfile profile, Command command, @Nullable Command expectedCommand) { + reset(mockCallback); + profile.onCommandFromHandler(command); + Command eC = expectedCommand; + if (eC == null) { + verifyNoInteractions(mockCallback); + } else { + verify(mockCallback, times(1)).sendCommand(eq(eC)); + } + } + + private void verifySendUpdate(StateProfile profile, State state, State expectedState) { + reset(mockCallback); + profile.onStateUpdateFromHandler(state); + verify(mockCallback, times(1)).sendUpdate(eq(expectedState)); + } +} diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactoryOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactoryOSGiTest.java index f4f867bf5..f04e10394 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactoryOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/profiles/SystemProfileFactoryOSGiTest.java @@ -50,7 +50,8 @@ import org.openhab.core.thing.type.ChannelType; public class SystemProfileFactoryOSGiTest extends JavaOSGiTest { private final Map properties = Map.of(SystemOffsetProfile.OFFSET_PARAM, BigDecimal.ZERO, - SystemHysteresisStateProfile.LOWER_PARAM, BigDecimal.TEN); + SystemHysteresisStateProfile.LOWER_PARAM, BigDecimal.TEN, SystemRangeStateProfile.UPPER_PARAM, + BigDecimal.valueOf(40)); private SystemProfileFactory profileFactory; @@ -68,7 +69,7 @@ public class SystemProfileFactoryOSGiTest extends JavaOSGiTest { @Test public void systemProfileTypesAndUidsShouldBeAvailable() { Collection systemProfileTypeUIDs = profileFactory.getSupportedProfileTypeUIDs(); - assertEquals(16, systemProfileTypeUIDs.size()); + assertEquals(17, systemProfileTypeUIDs.size()); Collection systemProfileTypes = profileFactory.getProfileTypes(null); assertEquals(systemProfileTypeUIDs.size(), systemProfileTypes.size());