From 2232b44064aaffc0e61605f589c3466b36247497 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 18:01:47 +0100 Subject: [PATCH 01/15] State filter profile Signed-off-by: Arne Seime --- .../README.md | 43 +++- .../config/StateFilterProfileConfig.java | 30 +++ .../factory/BasicProfilesFactory.java | 18 +- .../internal/profiles/StateFilterProfile.java | 202 ++++++++++++++++++ .../factory/BasicProfilesFactoryTest.java | 9 +- .../profiles/StateFilterProfileTest.java | 191 +++++++++++++++++ 6 files changed, 479 insertions(+), 14 deletions(-) create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java diff --git a/bundles/org.smarthomej.transform.basicprofiles/README.md b/bundles/org.smarthomej.transform.basicprofiles/README.md index 3d622f5751..d43022a9cf 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/README.md +++ b/bundles/org.smarthomej.transform.basicprofiles/README.md @@ -18,9 +18,9 @@ The given Command value is parsed either to `IncreaseDecreaseType`, `NextPreviou ```java Switch lightsStatus { - channel="hue:0200:XXX:1:color", + channel="hue:0200:XXX:1:color", channel="deconz:switch:YYY:1:buttonevent" [profile="basic-profiles:generic-command", events="1002,1003", command="ON"] -} + } ``` ## Generic Toggle Switch Profile @@ -37,9 +37,9 @@ The Generic Toggle Switch Profile is a specialization of the Generic Command Pro ```java Switch lightsStatus { - channel="hue:0200:XXX:1:color", + channel="hue:0200:XXX:1:color", channel="deconz:switch:YYY:1:buttonevent" [profile="basic-profiles:toggle-switch", events="1002,1003"] -} + } ``` ## Debounce (Counting) Profile @@ -87,7 +87,7 @@ It requires no specific configuration. The values of `QuantityType`, `PercentType` and `DecimalTypes` are negated (multiplied by `-1`). Otherwise the following mapping is used: - + `IncreaseDecreaseType`: `INCREASE` <-> `DECREASE` `NextPreviousType`: `NEXT` <-> `PREVIOUS` `OnOffType`: `ON` <-> `OFF` @@ -173,7 +173,36 @@ Possible values for parameter `restoreValue`: ```Java Switch motionSensorFirstFloor { - channel="deconz:presencesensor:XXX:YYY:presence", + channel="deconz:presencesensor:XXX:YYY:presence", channel="deconz:colortemperaturelight:AAA:BBB:brightness" [profile="basic-profiles:time-range-command", inRangeValue=100, outOfRangeValue=15, start="08:00", end="23:00", restoreValue="PREVIOUS"] -} + } +``` + +## State Filter Profile + +This filter passes on state updates from a (binding) handler to the item if and only if all listed item state conditions +are met (conditions are ANDed togegheter). +Option to instead pass different state update in case the conditions are not met. +State values may be quoted to treat as `StringType`. + +Use case: Ignore values from a binding unless some other item(s) have a specific state. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `conditions` | text | Comma separated list of expressions on the format `ITEM_NAME OPERATOR ITEM_STATE`, ie `MyItem EQ OFF`. Use quotes around `ITEM_STATE` to treat value as string ie `'OFF`' | +| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use quotes to treat as `StringType` | + +Possible values for token `OPERATOR` in `conditions`: + +- `EQ` - Equals +- `NEQ` - Not equals + +### Full Example + +```Java +Number:Temperature airconTemperature{ + channel="mybinding:mything:mychannel"[profile="basic-profiles:state-filter",conditions="airconPower_item EQ ON",mismatchState="UNDEF"] + } ``` diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java new file mode 100644 index 0000000000..006438f9b3 --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.UnDefType; +import org.smarthomej.transform.basicprofiles.internal.profiles.StateFilterProfile; + +/** + * Configuration class for {@link StateFilterProfile}. + * + * @author Arne Seime - Initial contribution + */ +@NonNullByDefault +public class StateFilterProfileConfig { + + public String conditions = ""; + + public String mismatchState = UnDefType.UNDEF.toString(); +} diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java index bed0cb6d86..c9142a7f06 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java @@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.i18n.LocalizedKey; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.thing.Channel; import org.openhab.core.thing.DefaultSystemChannelTypeProvider; @@ -49,6 +50,7 @@ import org.smarthomej.transform.basicprofiles.internal.profiles.GenericToggleSwitchTriggerProfile; import org.smarthomej.transform.basicprofiles.internal.profiles.InvertStateProfile; import org.smarthomej.transform.basicprofiles.internal.profiles.RoundStateProfile; +import org.smarthomej.transform.basicprofiles.internal.profiles.StateFilterProfile; import org.smarthomej.transform.basicprofiles.internal.profiles.ThresholdStateProfile; import org.smarthomej.transform.basicprofiles.internal.profiles.TimeRangeCommandProfile; @@ -69,6 +71,7 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider public static final ProfileTypeUID ROUND_UID = new ProfileTypeUID(SCOPE, "round"); public static final ProfileTypeUID THRESHOLD_UID = new ProfileTypeUID(SCOPE, "threshold"); public static final ProfileTypeUID TIME_RANGE_COMMAND_UID = new ProfileTypeUID(SCOPE, "time-range-command"); + public static final ProfileTypeUID STATE_FILTER_UID = new ProfileTypeUID(SCOPE, "state-filter"); private static final ProfileType PROFILE_TYPE_GENERIC_COMMAND = ProfileTypeBuilder .newTrigger(GENERIC_COMMAND_UID, "Generic Command") // @@ -102,24 +105,29 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider .withSupportedItemTypes(CoreItemFactory.SWITCH) // .withSupportedChannelTypeUIDs(DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_MOTION) // .build(); + private static final ProfileType PROFILE_STATE_FILTER = ProfileTypeBuilder + .newState(STATE_FILTER_UID, "Filter handler state updates based on any item state").build(); private static final Set SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GENERIC_COMMAND_UID, GENERIC_TOGGLE_SWITCH_UID, DEBOUNCE_COUNTING_UID, DEBOUNCE_TIME_UID, INVERT_UID, ROUND_UID, THRESHOLD_UID, - TIME_RANGE_COMMAND_UID); + TIME_RANGE_COMMAND_UID, STATE_FILTER_UID); private static final Set SUPPORTED_PROFILE_TYPES = Set.of(PROFILE_TYPE_GENERIC_COMMAND, PROFILE_TYPE_GENERIC_TOGGLE_SWITCH, PROFILE_TYPE_DEBOUNCE_COUNTING, PROFILE_TYPE_DEBOUNCE_TIME, - PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD, PROFILE_TYPE_TIME_RANGE_COMMAND); + PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD, PROFILE_TYPE_TIME_RANGE_COMMAND, + PROFILE_STATE_FILTER); private final Map localizedProfileTypeCache = new ConcurrentHashMap<>(); private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService; private final Bundle bundle; + private final ItemRegistry itemRegistry; @Activate public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService, - final @Reference BundleResolver bundleResolver) { + final @Reference BundleResolver bundleResolver, @Reference ItemRegistry itemRegistry) { this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService; - this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); + bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); + this.itemRegistry = itemRegistry; } @Override @@ -141,6 +149,8 @@ public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService return new ThresholdStateProfile(callback, context); } else if (TIME_RANGE_COMMAND_UID.equals(profileTypeUID)) { return new TimeRangeCommandProfile(callback, context); + } else if (STATE_FILTER_UID.equals(profileTypeUID)) { + return new StateFilterProfile(callback, context, itemRegistry); } return null; } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java new file mode 100644 index 0000000000..a3dbaa326c --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.transform.basicprofiles.internal.profiles; + +import static org.smarthomej.transform.basicprofiles.internal.factory.BasicProfilesFactory.STATE_FILTER_UID; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.types.StringType; +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.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TypeParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smarthomej.transform.basicprofiles.internal.config.StateFilterProfileConfig; + +/** + * Accepts updates to state as long as conditions are met. Support for sending fixed state if conditions are *not* + * met. + * + * @author Arne Seime - Initial contribution + */ +@NonNullByDefault +public class StateFilterProfile implements StateProfile { + + private final Logger logger = LoggerFactory.getLogger(StateFilterProfile.class); + + private final ItemRegistry itemRegistry; + private final ProfileCallback callback; + private List> acceptedDataTypes; + + private List conditions = new ArrayList<>(); + + @Nullable + private State configMismatchState = null; + + public StateFilterProfile(ProfileCallback callback, ProfileContext context, ItemRegistry itemRegistry) { + this.callback = callback; + acceptedDataTypes = context.getAcceptedDataTypes(); + this.itemRegistry = itemRegistry; + + StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class); + if (config != null) { + conditions.addAll(parseConditions(config.conditions)); + configMismatchState = parseState(config.mismatchState); + } + } + + private List parseConditions(String config) { + if (config == null) + return List.of(); + + List parsedConditions = new ArrayList<>(); + try { + String[] expressions = config.split(","); + for (String expression : expressions) { + String[] parts = expression.trim().split("\s"); + if (parts.length == 3) { + String itemName = parts[0]; + Condition.ComparisonType conditionType = Condition.ComparisonType + .valueOf(parts[1].toUpperCase(Locale.ROOT)); + String value = parts[2]; + parsedConditions.add(new Condition(itemName, conditionType, value)); + } + } + + return parsedConditions; + } catch (IllegalArgumentException e) { + logger.warn("Cannot parse condition {}. Expected format ITEM_NAME STATE_VALUE: {}", config, + e.getMessage()); + return List.of(); + } + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return STATE_FILTER_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + // do nothing + } + + @Override + public void onCommandFromItem(Command command) { + callback.handleCommand(command); + } + + @Override + public void onCommandFromHandler(Command command) { + callback.sendCommand(command); + } + + @Override + public void onStateUpdateFromHandler(State state) { + State resultState = checkCondition(state); + if (resultState != null) { + callback.sendUpdate(resultState); + } + } + + @Nullable + private State checkCondition(State state) { + if (!conditions.isEmpty()) { + boolean allConditionsMet = true; + for (Condition condition : conditions) { + try { + Item item = itemRegistry.getItem(condition.itemName); + String currentState = item.getState().toString(); + + switch (condition.comparisonType) { + case EQ: + if (!currentState.equals(condition.value)) { + allConditionsMet = false; + } + break; + case NEQ: { + if (currentState.equals(condition.value)) { + allConditionsMet = false; + } + break; + } + default: + logger.warn( + "Unknown condition type {} in condition {}. Expected 'eq' or 'neq' - skipping state update", + condition.comparisonType, condition); + allConditionsMet = false; + + } + } catch (ItemNotFoundException e) { + logger.warn( + "Cannot find item '{}' in registry - check your condition expression - skipping state update", + condition.itemName); + allConditionsMet = false; + } + + } + if (allConditionsMet) { + return state; + } else { + return configMismatchState; + } + } else { + logger.warn( + "No configuration defined for StateFilterProfile (check for log messages when instantiating profile) - skipping state update"); + } + + return null; + } + + @Nullable + State parseState(@Nullable String stateString) { + // Quoted strings are parsed as StringType + if (stateString == null) { + return null; + } else if (stateString.startsWith("'") && stateString.endsWith("'")) { + return new StringType(stateString.substring(1, stateString.length() - 1)); + } else { + return TypeParser.parseState(acceptedDataTypes, stateString); + } + } + + class Condition { + String itemName; + + ComparisonType comparisonType; + String value; + + public Condition(String itemName, ComparisonType comparisonType, String value) { + this.itemName = itemName; + this.comparisonType = comparisonType; + this.value = value; + } + + enum ComparisonType { + EQ, + NEQ + } + } +} diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java index 4e2703d5e7..ea0596bb74 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java @@ -14,7 +14,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import java.util.Collection; @@ -29,6 +30,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.config.core.Configuration; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.profiles.ProfileCallback; import org.openhab.core.thing.profiles.ProfileContext; @@ -51,7 +53,7 @@ @NonNullByDefault public class BasicProfilesFactoryTest { - private static final int NUMBER_OF_PROFILES = 8; + private static final int NUMBER_OF_PROFILES = 9; private static final Map PROPERTIES = Map.of(ThresholdStateProfile.PARAM_THRESHOLD, 15, RoundStateProfile.PARAM_SCALE, 2, GenericCommandTriggerProfile.PARAM_EVENTS, "1002,1003", @@ -63,12 +65,13 @@ public class BasicProfilesFactoryTest { private @Mock @NonNullByDefault({}) BundleResolver mockBundleResolver; private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; private @Mock @NonNullByDefault({}) ProfileContext mockContext; + private @Mock @NonNullByDefault({}) ItemRegistry mockItemRegistry; private @NonNullByDefault({}) BasicProfilesFactory profileFactory; @BeforeEach public void setup() { - profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver); + profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver, mockItemRegistry); when(mockContext.getConfiguration()).thenReturn(CONFIG); } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java new file mode 100644 index 0000000000..628cfeb288 --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.transform.basicprofiles.internal.profiles; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +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.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Basic unit tests for {@link StateFilterProfile}. + * + * @author Arne Seime - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class StateFilterProfileTest { + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + private @Mock @NonNullByDefault({}) ProfileContext mockContext; + private @Mock @NonNullByDefault({}) ItemRegistry mockItemRegistry; + + @BeforeEach + public void setup() { + reset(mockContext); + reset(mockCallback); + } + + @Test + public void testNoConditions() { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", ""))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testMalformedConditions() { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName invalid"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidComparatorConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName lt Value"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidItemConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidMultipleConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value,itemname invalid"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testSingleConditionMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } + + private Item stringItemWithState(String itemName, String value) { + StringItem item = new StringItem(itemName); + item.setState(new StringType(value)); + return item; + } + + @Test + public void testMultipleCondition_AllMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value, ItemName2 eq Value2"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + when(mockItemRegistry.getItem("ItemName2")).thenReturn(stringItemWithState("ItemName2", "Value2")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } + + @Test + public void testMultipleCondition_SingleMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value, ItemName2 eq Value2"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + when(mockItemRegistry.getItem("ItemName2")).thenThrow(ItemNotFoundException.class); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testFailingConditionWithMismatchState() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "UNDEF"))); + when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class)); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); + verify(mockCallback, times(1)).sendUpdate(eq(UnDefType.UNDEF)); + } + + @Test + void testParseStateNonQuotes() { + when(mockContext.getAcceptedDataTypes()) + .thenReturn(List.of(UnDefType.class, OnOffType.class, StringType.class)); + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", ""))); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + assertEquals(UnDefType.UNDEF, profile.parseState("UNDEF")); + assertEquals(new StringType("UNDEF"), profile.parseState("'UNDEF'")); + assertEquals(OnOffType.ON, profile.parseState("ON")); + assertEquals(new StringType("ON"), profile.parseState("'ON'")); + } +} From f385fed36ba3439e5e45ab05027e1bcb1d8ce64e Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 19:26:40 +0100 Subject: [PATCH 02/15] Add config description Signed-off-by: Arne Seime --- .../resources/OH-INF/config/state-filter.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml new file mode 100644 index 0000000000..3376951b40 --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml @@ -0,0 +1,17 @@ + + + + + + + Comma separated list of expressions on the format ITEM_NAME OPERATOR ITEM_STATE, ie "MyItem EQ OFF". Use quotes around ITEM_STATE to treat value as string ie "'OFF'". + + + + State to pass to item instead if conditions are NOT met. Use quotes to treat as `StringType` + + + From e7f4f89967ca032a847da510af35546004fcf0ee Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 19:31:10 +0100 Subject: [PATCH 03/15] More logging Signed-off-by: Arne Seime --- .../internal/profiles/StateFilterProfile.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index a3dbaa326c..74f2a539f5 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -82,6 +82,8 @@ private List parseConditions(String config) { .valueOf(parts[1].toUpperCase(Locale.ROOT)); String value = parts[2]; parsedConditions.add(new Condition(itemName, conditionType, value)); + } else { + logger.warn("Malformed condition expression: {}", expression); } } @@ -117,7 +119,10 @@ public void onCommandFromHandler(Command command) { public void onStateUpdateFromHandler(State state) { State resultState = checkCondition(state); if (resultState != null) { + logger.debug("Received state update from handler: {}, forwarded as ", state,resultState); callback.sendUpdate(resultState); + } else { + logger.debug("Received state update from handler: {}, not forwarded to item", state); } } @@ -126,6 +131,7 @@ private State checkCondition(State state) { if (!conditions.isEmpty()) { boolean allConditionsMet = true; for (Condition condition : conditions) { + logger.debug("Evaluting condition: {}", condition); try { Item item = itemRegistry.getItem(condition.itemName); String currentState = item.getState().toString(); @@ -198,5 +204,14 @@ enum ComparisonType { EQ, NEQ } + + @Override + public String toString() { + return "Condition{" + + "itemName='" + itemName + '\'' + + ", comparisonType=" + comparisonType + + ", value='" + value + '\'' + + '}'; + } } } From 3b3e0a7bbbe0c7833e29a098c63582b87f0dd2c8 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 19:48:17 +0100 Subject: [PATCH 04/15] Refactor Signed-off-by: Arne Seime --- .../internal/profiles/StateFilterProfile.java | 54 ++++++++++++------- .../profiles/StateFilterProfileTest.java | 23 ++++++++ 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 74f2a539f5..c0e2d6d98d 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -119,7 +119,7 @@ public void onCommandFromHandler(Command command) { public void onStateUpdateFromHandler(State state) { State resultState = checkCondition(state); if (resultState != null) { - logger.debug("Received state update from handler: {}, forwarded as ", state,resultState); + logger.debug("Received state update from handler: {}, forwarded as {}", state,resultState); callback.sendUpdate(resultState); } else { logger.debug("Received state update from handler: {}, not forwarded to item", state); @@ -134,26 +134,10 @@ private State checkCondition(State state) { logger.debug("Evaluting condition: {}", condition); try { Item item = itemRegistry.getItem(condition.itemName); - String currentState = item.getState().toString(); - - switch (condition.comparisonType) { - case EQ: - if (!currentState.equals(condition.value)) { - allConditionsMet = false; - } - break; - case NEQ: { - if (currentState.equals(condition.value)) { - allConditionsMet = false; - } - break; - } - default: - logger.warn( - "Unknown condition type {} in condition {}. Expected 'eq' or 'neq' - skipping state update", - condition.comparisonType, condition); - allConditionsMet = false; + String itemState = item.getState().toString(); + if(!condition.matches(itemState)) { + allConditionsMet = false; } } catch (ItemNotFoundException e) { logger.warn( @@ -194,10 +178,40 @@ class Condition { ComparisonType comparisonType; String value; + boolean quoted = false; + public Condition(String itemName, ComparisonType comparisonType, String value) { this.itemName = itemName; this.comparisonType = comparisonType; this.value = value; + this.quoted = value.startsWith("'") && value.endsWith("'"); + if (quoted) { + this.value = value.substring(1, value.length() - 1); + } + } + + public boolean matches(String state) { + switch (comparisonType) { + case EQ: + if (!state.equals(value)) { + return false; + } + break; + case NEQ: { + if (state.equals(value)) { + return false; + } + break; + } + default: + logger.warn( + "Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update", + comparisonType); + return false; + + } + return true; + } enum ComparisonType { diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java index 628cfeb288..cbd445a85d 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -128,6 +128,17 @@ public void testSingleConditionMatch() throws ItemNotFoundException { profile.onStateUpdateFromHandler(expectation); verify(mockCallback, times(1)).sendUpdate(eq(expectation)); } + @Test + public void testSingleConditionMatchQuoted() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value'"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } private Item stringItemWithState(String itemName, String value) { StringItem item = new StringItem(itemName); @@ -175,6 +186,18 @@ public void testFailingConditionWithMismatchState() throws ItemNotFoundException profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); verify(mockCallback, times(1)).sendUpdate(eq(UnDefType.UNDEF)); } + @Test + public void testFailingConditionWithMismatchStateQuoted() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "'UNDEF'"))); + when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class)); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); + verify(mockCallback, times(1)).sendUpdate(eq(new StringType("UNDEF"))); + } @Test void testParseStateNonQuotes() { From 742f82b997b42ee07f7bdfbb542b281e9070b2fd Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 20:05:34 +0100 Subject: [PATCH 05/15] Logging Signed-off-by: Arne Seime --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index c0e2d6d98d..4c7086be04 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -83,13 +83,13 @@ private List parseConditions(String config) { String value = parts[2]; parsedConditions.add(new Condition(itemName, conditionType, value)); } else { - logger.warn("Malformed condition expression: {}", expression); + logger.warn("Malformed condition expression: '{}'", expression); } } return parsedConditions; } catch (IllegalArgumentException e) { - logger.warn("Cannot parse condition {}. Expected format ITEM_NAME STATE_VALUE: {}", config, + logger.warn("Cannot parse condition {}. Expected format ITEM_NAME STATE_VALUE: '{}'", config, e.getMessage()); return List.of(); } From ce87b16a93474dc35265b24973944c22751dc9d7 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 20:42:40 +0100 Subject: [PATCH 06/15] Formatting Signed-off-by: Arne Seime --- .../README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/README.md b/bundles/org.smarthomej.transform.basicprofiles/README.md index d43022a9cf..3982ac5438 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/README.md +++ b/bundles/org.smarthomej.transform.basicprofiles/README.md @@ -18,9 +18,9 @@ The given Command value is parsed either to `IncreaseDecreaseType`, `NextPreviou ```java Switch lightsStatus { - channel="hue:0200:XXX:1:color", + channel="hue:0200:XXX:1:color", channel="deconz:switch:YYY:1:buttonevent" [profile="basic-profiles:generic-command", events="1002,1003", command="ON"] - } +} ``` ## Generic Toggle Switch Profile @@ -37,9 +37,9 @@ The Generic Toggle Switch Profile is a specialization of the Generic Command Pro ```java Switch lightsStatus { - channel="hue:0200:XXX:1:color", + channel="hue:0200:XXX:1:color", channel="deconz:switch:YYY:1:buttonevent" [profile="basic-profiles:toggle-switch", events="1002,1003"] - } +} ``` ## Debounce (Counting) Profile @@ -87,7 +87,7 @@ It requires no specific configuration. The values of `QuantityType`, `PercentType` and `DecimalTypes` are negated (multiplied by `-1`). Otherwise the following mapping is used: - + `IncreaseDecreaseType`: `INCREASE` <-> `DECREASE` `NextPreviousType`: `NEXT` <-> `PREVIOUS` `OnOffType`: `ON` <-> `OFF` @@ -173,15 +173,15 @@ Possible values for parameter `restoreValue`: ```Java Switch motionSensorFirstFloor { - channel="deconz:presencesensor:XXX:YYY:presence", + channel="deconz:presencesensor:XXX:YYY:presence", channel="deconz:colortemperaturelight:AAA:BBB:brightness" [profile="basic-profiles:time-range-command", inRangeValue=100, outOfRangeValue=15, start="08:00", end="23:00", restoreValue="PREVIOUS"] - } +} ``` ## State Filter Profile This filter passes on state updates from a (binding) handler to the item if and only if all listed item state conditions -are met (conditions are ANDed togegheter). +are met (conditions are ANDed together). Option to instead pass different state update in case the conditions are not met. State values may be quoted to treat as `StringType`. @@ -204,5 +204,5 @@ Possible values for token `OPERATOR` in `conditions`: ```Java Number:Temperature airconTemperature{ channel="mybinding:mything:mychannel"[profile="basic-profiles:state-filter",conditions="airconPower_item EQ ON",mismatchState="UNDEF"] - } +} ``` From 0d3aa873cf953e1333caaaa00d082851629182ec Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 20:47:34 +0100 Subject: [PATCH 07/15] Minor Signed-off-by: Arne Seime --- .../basicprofiles/internal/factory/BasicProfilesFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java index c9142a7f06..03b92e8e2f 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java @@ -126,7 +126,7 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService, final @Reference BundleResolver bundleResolver, @Reference ItemRegistry itemRegistry) { this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService; - bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); + this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); this.itemRegistry = itemRegistry; } From 091063e2758b41a7446fd169b4fb7b00bd8db9ef Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 29 Oct 2023 20:54:27 +0100 Subject: [PATCH 08/15] Spotless Signed-off-by: Arne Seime --- .../internal/profiles/StateFilterProfile.java | 15 +++++---------- .../main/resources/OH-INF/config/state-filter.xml | 3 ++- .../internal/profiles/StateFilterProfileTest.java | 2 ++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 4c7086be04..0c5d3ff158 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -119,7 +119,7 @@ public void onCommandFromHandler(Command command) { public void onStateUpdateFromHandler(State state) { State resultState = checkCondition(state); if (resultState != null) { - logger.debug("Received state update from handler: {}, forwarded as {}", state,resultState); + logger.debug("Received state update from handler: {}, forwarded as {}", state, resultState); callback.sendUpdate(resultState); } else { logger.debug("Received state update from handler: {}, not forwarded to item", state); @@ -136,7 +136,7 @@ private State checkCondition(State state) { Item item = itemRegistry.getItem(condition.itemName); String itemState = item.getState().toString(); - if(!condition.matches(itemState)) { + if (!condition.matches(itemState)) { allConditionsMet = false; } } catch (ItemNotFoundException e) { @@ -204,14 +204,12 @@ public boolean matches(String state) { break; } default: - logger.warn( - "Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update", + logger.warn("Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update", comparisonType); return false; } return true; - } enum ComparisonType { @@ -221,11 +219,8 @@ enum ComparisonType { @Override public String toString() { - return "Condition{" + - "itemName='" + itemName + '\'' + - ", comparisonType=" + comparisonType + - ", value='" + value + '\'' + - '}'; + return "Condition{" + "itemName='" + itemName + '\'' + ", comparisonType=" + comparisonType + ", value='" + + value + '\'' + '}'; } } } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml index 3376951b40..c468dcd9fd 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml @@ -7,7 +7,8 @@ - Comma separated list of expressions on the format ITEM_NAME OPERATOR ITEM_STATE, ie "MyItem EQ OFF". Use quotes around ITEM_STATE to treat value as string ie "'OFF'". + Comma separated list of expressions on the format ITEM_NAME OPERATOR ITEM_STATE, ie "MyItem EQ OFF". Use + quotes around ITEM_STATE to treat value as string ie "'OFF'". diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java index cbd445a85d..b98bd23b53 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -128,6 +128,7 @@ public void testSingleConditionMatch() throws ItemNotFoundException { profile.onStateUpdateFromHandler(expectation); verify(mockCallback, times(1)).sendUpdate(eq(expectation)); } + @Test public void testSingleConditionMatchQuoted() throws ItemNotFoundException { when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value'"))); @@ -186,6 +187,7 @@ public void testFailingConditionWithMismatchState() throws ItemNotFoundException profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); verify(mockCallback, times(1)).sendUpdate(eq(UnDefType.UNDEF)); } + @Test public void testFailingConditionWithMismatchStateQuoted() throws ItemNotFoundException { when(mockContext.getConfiguration()) From b2c2ca7fc963db3b8482d169622d9b6d3da6be65 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 3 Dec 2023 13:42:07 +0100 Subject: [PATCH 09/15] Rename condition to statecondition Signed-off-by: Arne Seime --- .../README.md | 2 +- .../internal/profiles/StateFilterProfile.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/README.md b/bundles/org.smarthomej.transform.basicprofiles/README.md index 3982ac5438..6da4462631 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/README.md +++ b/bundles/org.smarthomej.transform.basicprofiles/README.md @@ -192,7 +192,7 @@ Use case: Ignore values from a binding unless some other item(s) have a specific | Configuration Parameter | Type | Description | |-------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `conditions` | text | Comma separated list of expressions on the format `ITEM_NAME OPERATOR ITEM_STATE`, ie `MyItem EQ OFF`. Use quotes around `ITEM_STATE` to treat value as string ie `'OFF`' | -| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use quotes to treat as `StringType` | +| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use quotes to treat as `StringType`. Defaults to `UNDEF` | Possible values for token `OPERATOR` in `conditions`: diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 0c5d3ff158..8cfc753268 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -50,7 +50,7 @@ public class StateFilterProfile implements StateProfile { private final ProfileCallback callback; private List> acceptedDataTypes; - private List conditions = new ArrayList<>(); + private List conditions = new ArrayList<>(); @Nullable private State configMismatchState = null; @@ -71,17 +71,17 @@ private List parseConditions(String config) { if (config == null) return List.of(); - List parsedConditions = new ArrayList<>(); + List parsedConditions = new ArrayList<>(); try { String[] expressions = config.split(","); for (String expression : expressions) { String[] parts = expression.trim().split("\s"); if (parts.length == 3) { String itemName = parts[0]; - Condition.ComparisonType conditionType = Condition.ComparisonType + StateCondition.ComparisonType conditionType = StateCondition.ComparisonType .valueOf(parts[1].toUpperCase(Locale.ROOT)); String value = parts[2]; - parsedConditions.add(new Condition(itemName, conditionType, value)); + parsedConditions.add(new StateCondition(itemName, conditionType, value)); } else { logger.warn("Malformed condition expression: '{}'", expression); } @@ -130,7 +130,7 @@ public void onStateUpdateFromHandler(State state) { private State checkCondition(State state) { if (!conditions.isEmpty()) { boolean allConditionsMet = true; - for (Condition condition : conditions) { + for (StateCondition condition : conditions) { logger.debug("Evaluting condition: {}", condition); try { Item item = itemRegistry.getItem(condition.itemName); @@ -172,7 +172,7 @@ State parseState(@Nullable String stateString) { } } - class Condition { + class StateCondition { String itemName; ComparisonType comparisonType; @@ -180,7 +180,7 @@ class Condition { boolean quoted = false; - public Condition(String itemName, ComparisonType comparisonType, String value) { + public StateCondition(String itemName, ComparisonType comparisonType, String value) { this.itemName = itemName; this.comparisonType = comparisonType; this.value = value; @@ -219,7 +219,7 @@ enum ComparisonType { @Override public String toString() { - return "Condition{" + "itemName='" + itemName + '\'' + ", comparisonType=" + comparisonType + ", value='" + return "StateCondition{" + "itemName='" + itemName + '\'' + ", comparisonType=" + comparisonType + ", value='" + value + '\'' + '}'; } } From 5724ec347cc91cf2d929859597cafca497269fa7 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sun, 3 Dec 2023 14:02:24 +0100 Subject: [PATCH 10/15] Updates based on feedback Signed-off-by: Arne Seime --- .../README.md | 10 +++++---- .../config/StateFilterProfileConfig.java | 2 ++ .../internal/profiles/StateFilterProfile.java | 21 +++++++------------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/README.md b/bundles/org.smarthomej.transform.basicprofiles/README.md index 6da4462631..7b5e4ee074 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/README.md +++ b/bundles/org.smarthomej.transform.basicprofiles/README.md @@ -189,16 +189,18 @@ Use case: Ignore values from a binding unless some other item(s) have a specific ### Configuration -| Configuration Parameter | Type | Description | -|-------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `conditions` | text | Comma separated list of expressions on the format `ITEM_NAME OPERATOR ITEM_STATE`, ie `MyItem EQ OFF`. Use quotes around `ITEM_STATE` to treat value as string ie `'OFF`' | -| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use quotes to treat as `StringType`. Defaults to `UNDEF` | +| Configuration Parameter | Type | Description | +|-------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `conditions` | text | Comma separated list of expressions on the format `ITEM_NAME OPERATOR ITEM_STATE`, ie `MyItem EQ OFF`. Use quotes around `ITEM_STATE` to treat value as string ie `'OFF'` and not `OnOffType.OFF` | +| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use single quotes to treat as `StringType`. Defaults to `UNDEF` | +| `separator` | text | Optional separator string to separate expressions when using multiple. Defaults to `,` | Possible values for token `OPERATOR` in `conditions`: - `EQ` - Equals - `NEQ` - Not equals + ### Full Example ```Java diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java index 006438f9b3..77f1c81f0c 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java @@ -27,4 +27,6 @@ public class StateFilterProfileConfig { public String conditions = ""; public String mismatchState = UnDefType.UNDEF.toString(); + + public String separator = ","; } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 8cfc753268..0e5eb83890 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -62,18 +62,18 @@ public StateFilterProfile(ProfileCallback callback, ProfileContext context, Item StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class); if (config != null) { - conditions.addAll(parseConditions(config.conditions)); + conditions.addAll(parseConditions(config.conditions, config.separator)); configMismatchState = parseState(config.mismatchState); } } - private List parseConditions(String config) { + private List parseConditions(@Nullable String config, String separator) { if (config == null) return List.of(); List parsedConditions = new ArrayList<>(); try { - String[] expressions = config.split(","); + String[] expressions = config.split(separator); for (String expression : expressions) { String[] parts = expression.trim().split("\s"); if (parts.length == 3) { @@ -193,15 +193,9 @@ public StateCondition(String itemName, ComparisonType comparisonType, String val public boolean matches(String state) { switch (comparisonType) { case EQ: - if (!state.equals(value)) { - return false; - } - break; + return state.equals(value); case NEQ: { - if (state.equals(value)) { - return false; - } - break; + return !state.equals(value); } default: logger.warn("Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update", @@ -209,7 +203,6 @@ public boolean matches(String state) { return false; } - return true; } enum ComparisonType { @@ -219,8 +212,8 @@ enum ComparisonType { @Override public String toString() { - return "StateCondition{" + "itemName='" + itemName + '\'' + ", comparisonType=" + comparisonType + ", value='" - + value + '\'' + '}'; + return "Condition{itemName='" + itemName + "', comparisonType=" + comparisonType + ", value='" + + value + "'}'"; } } } From f8d82ff41b9bf6c7b20199079f7891a8fe7dace6 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Mon, 4 Dec 2023 08:16:51 +0100 Subject: [PATCH 11/15] Spotless Signed-off-by: Arne Seime --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 0e5eb83890..4f0cd46dae 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -212,8 +212,8 @@ enum ComparisonType { @Override public String toString() { - return "Condition{itemName='" + itemName + "', comparisonType=" + comparisonType + ", value='" - + value + "'}'"; + return "Condition{itemName='" + itemName + "', comparisonType=" + comparisonType + ", value='" + value + + "'}'"; } } } From cd29402b0ac4abd7b2823399389588377fae3f8b Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 21 Jan 2024 19:27:48 +0100 Subject: [PATCH 12/15] Update bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 4f0cd46dae..d007e1fdc5 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -52,8 +52,7 @@ public class StateFilterProfile implements StateProfile { private List conditions = new ArrayList<>(); - @Nullable - private State configMismatchState = null; + private @Nullable State configMismatchState = null; public StateFilterProfile(ProfileCallback callback, ProfileContext context, ItemRegistry itemRegistry) { this.callback = callback; From 37e6c580b3cbb87ab1871a2a8d6b310524f79a03 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 21 Jan 2024 19:28:31 +0100 Subject: [PATCH 13/15] Update bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index d007e1fdc5..d76f875286 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -61,7 +61,7 @@ public StateFilterProfile(ProfileCallback callback, ProfileContext context, Item StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class); if (config != null) { - conditions.addAll(parseConditions(config.conditions, config.separator)); + conditions = parseConditions(config.conditions, config.separator); configMismatchState = parseState(config.mismatchState); } } From f13f3f34f95a35273ac1ab57ce8dcfedb52a8605 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 21 Jan 2024 19:28:40 +0100 Subject: [PATCH 14/15] Update bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index d76f875286..36b779c781 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -67,8 +67,9 @@ public StateFilterProfile(ProfileCallback callback, ProfileContext context, Item } private List parseConditions(@Nullable String config, String separator) { - if (config == null) + if (config == null) { return List.of(); + } List parsedConditions = new ArrayList<>(); try { From be7c2a77fc988a8086019e3214464c8cc3ea67ac Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 21 Jan 2024 19:28:46 +0100 Subject: [PATCH 15/15] Update bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java --- .../basicprofiles/internal/profiles/StateFilterProfile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 36b779c781..7cc1f71deb 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -50,7 +50,7 @@ public class StateFilterProfile implements StateProfile { private final ProfileCallback callback; private List> acceptedDataTypes; - private List conditions = new ArrayList<>(); + private List conditions = List.of(); private @Nullable State configMismatchState = null;