From 048c71423466078c39c317e8ac51bdbb49153072 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Sun, 12 May 2024 18:34:28 +0200 Subject: [PATCH 1/4] [basicprofiles] Initial contribution A set of basic profiles with general use cases. See documentation for details. Also-By: Christoph Weitkamp Also-By: Arne Seime Signed-off-by: Jan N. Klug --- .../NOTICE | 32 +++ .../README.md | 210 +++++++++++++++++ .../pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../internal/BasicProfilesConstants.java | 25 ++ .../DebounceCountingStateProfileConfig.java | 26 +++ .../DebounceTimeStateProfileConfig.java | 38 +++ .../config/RoundStateProfileConfig.java | 30 +++ .../config/StateFilterProfileConfig.java | 32 +++ .../config/ThresholdStateProfileConfig.java | 26 +++ .../config/TimeRangeCommandProfileConfig.java | 30 +++ .../factory/BasicProfilesFactory.java | 197 ++++++++++++++++ .../profiles/AbstractTriggerProfile.java | 63 +++++ .../DebounceCountingStateProfile.java | 106 +++++++++ .../profiles/DebounceTimeStateProfile.java | 181 +++++++++++++++ .../GenericCommandTriggerProfile.java | 92 ++++++++ .../GenericToggleSwitchTriggerProfile.java | 57 +++++ .../internal/profiles/InvertStateProfile.java | 119 ++++++++++ .../internal/profiles/RoundStateProfile.java | 127 ++++++++++ .../internal/profiles/StateFilterProfile.java | 219 ++++++++++++++++++ .../profiles/ThresholdStateProfile.java | 86 +++++++ .../profiles/TimeRangeCommandProfile.java | 217 +++++++++++++++++ .../src/main/resources/OH-INF/addon/addon.xml | 10 + .../main/resources/OH-INF/config/debounce.xml | 36 +++ .../OH-INF/config/generic-command.xml | 34 +++ .../main/resources/OH-INF/config/round.xml | 27 +++ .../resources/OH-INF/config/state-filter.xml | 18 ++ .../resources/OH-INF/config/threshold.xml | 14 ++ .../OH-INF/config/time-range-command.xml | 43 ++++ .../resources/OH-INF/config/toggle-switch.xml | 13 ++ .../OH-INF/i18n/basicprofiles.properties | 49 ++++ .../OH-INF/i18n/basicprofiles_de.properties | 49 ++++ .../factory/BasicProfilesFactoryTest.java | 98 ++++++++ .../DebounceCountingStateProfileTest.java | 143 ++++++++++++ .../GenericCommandTriggerProfileTest.java | 106 +++++++++ ...GenericToggleSwitchTriggerProfileTest.java | 94 ++++++++ .../profiles/InvertStateProfileTest.java | 121 ++++++++++ .../profiles/RoundStateProfileTest.java | 157 +++++++++++++ .../profiles/StateFilterProfileTest.java | 216 +++++++++++++++++ .../profiles/ThresholdStateProfileTest.java | 111 +++++++++ bundles/pom.xml | 1 + 41 files changed, 3279 insertions(+) create mode 100644 bundles/org.openhab.transform.basicprofiles/NOTICE create mode 100644 bundles/org.openhab.transform.basicprofiles/README.md create mode 100644 bundles/org.openhab.transform.basicprofiles/pom.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/BasicProfilesConstants.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceCountingStateProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceTimeStateProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/RoundStateProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/StateFilterProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/ThresholdStateProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/TimeRangeCommandProfileConfig.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/AbstractTriggerProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceTimeStateProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/debounce.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/generic-command.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/round.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/threshold.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/time-range-command.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/toggle-switch.xml create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles.properties create mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java create mode 100644 bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfileTest.java diff --git a/bundles/org.openhab.transform.basicprofiles/NOTICE b/bundles/org.openhab.transform.basicprofiles/NOTICE new file mode 100644 index 0000000000000..2e65467d8a956 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/NOTICE @@ -0,0 +1,32 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +== Third-party Content + +Parts of this code have been forked from https://github.com/smarthomej/addons + +Original license header of forked files was + +/** + * 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 + */ diff --git a/bundles/org.openhab.transform.basicprofiles/README.md b/bundles/org.openhab.transform.basicprofiles/README.md new file mode 100644 index 0000000000000..7b5e4ee07458c --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/README.md @@ -0,0 +1,210 @@ +# Basic Profiles + +This bundle provides a list of useful Profiles. + +## Generic Command Profile + +This Profile can be used to send a Command towards the Item when one event of a specified event list is triggered. +The given Command value is parsed either to `IncreaseDecreaseType`, `NextPreviousType`, `OnOffType`, `PlayPauseType`, `RewindFastforwardType`, `StopMoveType`, `UpDownType` or a `StringType` is used. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|------|----------------------------------------------------------------------------------| +| `events` | text | Comma separated list of events to which the profile should listen. **mandatory** | +| `command` | text | Command which should be sent if the event is triggered. **mandatory** | + +### Full Example + +```java +Switch lightsStatus { + 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 + +The Generic Toggle Switch Profile is a specialization of the Generic Command Profile and toggles the State of a Switch Item whenever one of the specified events is triggered. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|------|----------------------------------------------------------------------------------| +| `events` | text | Comma separated list of events to which the profile should listen. **mandatory** | + +### Full Example + +```java +Switch lightsStatus { + channel="hue:0200:XXX:1:color", + channel="deconz:switch:YYY:1:buttonevent" [profile="basic-profiles:toggle-switch", events="1002,1003"] +} +``` + +## Debounce (Counting) Profile + +This Profile counts and skips a user-defined number of State changes before it sends an update to the Item. +It can be used to debounce Item States. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|---------|-----------------------------------------------| +| `numberOfChanges` | integer | Number of changes before updating Item State. | + +### Full Example + +```java +Switch debouncedSwitch { channel="xxx" [profile="basic-profiles:debounce-counting", numberOfChanges=2] } +``` + +## Debounce (Time) Profile + +In `LAST` mode this profile delays commands or state updates for a configured number of milliseconds and only send the value if no other value is received with that timespan. +In `FIRST` mode this profile discards values for the configured time after a value is send. + +It can be used to debounce Item States/Commands or prevent excessive load on networks. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|---------|-----------------------------------------------| +| `toItemDelay` | integer | Timespan in ms before a received value is send to the item. | +| `toHandlerDelay` | integer | Timespan in ms before a received command is passed to the handler. | +| `mode` | text | `FIRST` (sends the first value received and discards later values), `LAST` (sends the last value received, discarding earlier values). | + +### Full Example + +```java +Number:Temperature debouncedSetpoint { channel="xxx" [profile="basic-profiles:debounce-time", toHandlerDelay=1000] } +``` + +## Invert / Negate Profile + +The Invert / Negate Profile inverts or negates a Command / State. +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` +`OpenClosedType`: `OPEN` <-> `CLOSED` +`PlayPauseType`: `PLAY` <-> `PAUSE` +`RewindFastforwardType`: `REWIND` <-> `FASTFORWARD` +`StopMoveType`: `MOVE` <-> `STOP` +`UpDownType`: `UP` <-> `DOWN` + +### Full Example + +```java +Switch invertedSwitch { channel="xxx" [profile="basic-profiles:invert"] } +``` + +## Round Profile + +The Round Profile scales the State to a specific number of decimal places based on the power of ten. +Optionally the [Rounding mode](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/RoundingMode.html) can be set. +Source Channels should accept Item Type `Number`. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|---------|-----------------------------------------------------------------------------------------------------------------| +| `scale` | integer | Scale to indicate the resulting number of decimal places (min: -16, max: 16, STEP: 1) **mandatory**. | +| `mode` | text | Rounding mode to be used (e.g. "UP", "DOWN", "CEILING", "FLOOR", "HALF_UP" or "HALF_DOWN" (default: "HALF_UP"). | + +### Full Example + +```java +Number roundedNumber { channel="xxx" [profile="basic-profiles:round", scale=0] } +Number:Temperature roundedTemperature { channel="xxx" [profile="basic-profiles:round", scale=1] } +``` + +## Threshold Profile + +The Threshold Profile triggers `ON` or `OFF` behavior when being linked to a Switch item if value is below a given threshold (default: 10). +A good use case for this Profile is a battery low indication. +Source Channels should accept Item Type `Dimmer` or `Number`. + +::: tip Note +This profile is a shortcut for the System Hysteresis Profile. +::: + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|---------|-----------------------------------------------------------------------------------------------------| +| `threshold` | integer | Triggers `ON` if value is below the given threshold, otherwise OFF (default: 10, min: 0, max: 100). | + +### Full Example + +```java +Switch thresholdItem { channel="xxx" [profile="basic-profiles:threshold", threshold=15] } +``` + +## Time Range Command Profile + +This is an enhanced implementation of a follow profile which converts `OnOffType` to a `PercentType`. +The value of the percent type can be different between a specific time of the day. +A possible use-case is switching lights (using a presence detector) with different intensities at day and at night. +Be aware: a range beyond midnight (e.g. start="23:00", end="01:00") is not yet supported. + +### Configuration + +| Configuration Parameter | Type | Description | +|-------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `inRangeValue` | integer | The value which will be send when the profile detects ON and current time is between start time and end time (default: 100, min: 0, max: 100). | +| `outOfRangeValue` | integer | The value which will be send when the profile detects ON and current time is NOT between start time and end time (default: 30, min: 0, max: 100). | +| `start` | text | The start time of the day (hh:mm). | +| `end` | text | The end time of the day (hh:mm). | +| `restoreValue` | text | Select what should happen when the profile detects OFF again (default: OFF). | + +Possible values for parameter `restoreValue`: + +- `OFF` - Turn the light off +- `NOTHING` - Do nothing +- `PREVIOUS` - Return to previous value +- `0` - `100` - Set a user-defined percent value + +### Full Example + +```Java +Switch motionSensorFirstFloor { + 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 together). +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'` 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 +Number:Temperature airconTemperature{ + channel="mybinding:mything:mychannel"[profile="basic-profiles:state-filter",conditions="airconPower_item EQ ON",mismatchState="UNDEF"] +} +``` diff --git a/bundles/org.openhab.transform.basicprofiles/pom.xml b/bundles/org.openhab.transform.basicprofiles/pom.xml new file mode 100644 index 0000000000000..8ccc07de4e4d6 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.transform.basicprofiles + + openHAB Add-ons :: Bundles :: Transformation Service :: Basic Profiles + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/feature/feature.xml b/bundles/org.openhab.transform.basicprofiles/src/main/feature/feature.xml new file mode 100644 index 0000000000000..df5577c02f202 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.transform.basicprofiles/${project.version} + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/BasicProfilesConstants.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/BasicProfilesConstants.java new file mode 100644 index 0000000000000..fbea317e32423 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/BasicProfilesConstants.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BasicProfilesConstants} class defines common constants, which are used across the whole bundle. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class BasicProfilesConstants { + public static final String SCOPE = "basic-profiles"; +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceCountingStateProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceCountingStateProfileConfig.java new file mode 100644 index 0000000000000..92fb647e00c47 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceCountingStateProfileConfig.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.transform.basicprofiles.internal.profiles.DebounceCountingStateProfile; + +/** + * Configuration for {@link DebounceCountingStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class DebounceCountingStateProfileConfig { + public int numberOfChanges = 1; +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceTimeStateProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceTimeStateProfileConfig.java new file mode 100644 index 0000000000000..5153ea070038e --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/DebounceTimeStateProfileConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Configuration for {@link org.openhab.transform.basicprofiles.internal.profiles.DebounceTimeStateProfile}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DebounceTimeStateProfileConfig { + public int toHandlerDelay = 0; + public int toItemDelay = 0; + public DebounceMode mode = DebounceMode.LAST; + + @Override + public String toString() { + return "DebounceTimeStateProfileConfig{toHandlerDelay=" + toHandlerDelay + ", toItemDelay=" + toItemDelay + + ", mode=" + mode + "}"; + } + + public enum DebounceMode { + FIRST, + LAST + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/RoundStateProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/RoundStateProfileConfig.java new file mode 100644 index 0000000000000..00cc2b65f4eb0 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/RoundStateProfileConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import java.math.RoundingMode; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.transform.basicprofiles.internal.profiles.RoundStateProfile; + +/** + * Configuration for {@link RoundStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class RoundStateProfileConfig { + public @Nullable Integer scale; + public String mode = RoundingMode.HALF_UP.name(); +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/StateFilterProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/StateFilterProfileConfig.java new file mode 100644 index 0000000000000..89429374bd4a8 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/StateFilterProfileConfig.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.UnDefType; +import org.openhab.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(); + + public String separator = ","; +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/ThresholdStateProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/ThresholdStateProfileConfig.java new file mode 100644 index 0000000000000..62037d84bfa24 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/ThresholdStateProfileConfig.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.transform.basicprofiles.internal.profiles.ThresholdStateProfile; + +/** + * Configuration for {@link ThresholdStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class ThresholdStateProfileConfig { + public int threshold = 10; +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/TimeRangeCommandProfileConfig.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/TimeRangeCommandProfileConfig.java new file mode 100644 index 0000000000000..d1800aeb92b3b --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/config/TimeRangeCommandProfileConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.transform.basicprofiles.internal.profiles.TimeRangeCommandProfile; + +/** + * Configuration for {@link TimeRangeCommandProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class TimeRangeCommandProfileConfig { + public int inRangeValue = 100; + public int outOfRangeValue = 30; + public @NonNullByDefault({}) String start; + public @NonNullByDefault({}) String end; + public String restoreValue = TimeRangeCommandProfile.CONFIG_RESTORE_VALUE_OFF; +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java new file mode 100644 index 0000000000000..2e7c53f9388fb --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.factory; + +import static org.openhab.transform.basicprofiles.internal.BasicProfilesConstants.SCOPE; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +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; +import org.openhab.core.thing.profiles.Profile; +import org.openhab.core.thing.profiles.ProfileAdvisor; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.ProfileFactory; +import org.openhab.core.thing.profiles.ProfileType; +import org.openhab.core.thing.profiles.ProfileTypeBuilder; +import org.openhab.core.thing.profiles.ProfileTypeProvider; +import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.util.BundleResolver; +import org.openhab.transform.basicprofiles.internal.profiles.DebounceCountingStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.DebounceTimeStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.GenericCommandTriggerProfile; +import org.openhab.transform.basicprofiles.internal.profiles.GenericToggleSwitchTriggerProfile; +import org.openhab.transform.basicprofiles.internal.profiles.InvertStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.RoundStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.StateFilterProfile; +import org.openhab.transform.basicprofiles.internal.profiles.ThresholdStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.TimeRangeCommandProfile; +import org.osgi.framework.Bundle; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link BasicProfilesFactory} is responsible for creating profiles. + * + * @author Christoph Weitkamp - Initial contribution + */ +@Component(service = { ProfileFactory.class, ProfileTypeProvider.class }) +@NonNullByDefault +public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider, ProfileAdvisor { + + public static final ProfileTypeUID GENERIC_COMMAND_UID = new ProfileTypeUID(SCOPE, "generic-command"); + public static final ProfileTypeUID GENERIC_TOGGLE_SWITCH_UID = new ProfileTypeUID(SCOPE, "toggle-switch"); + public static final ProfileTypeUID DEBOUNCE_COUNTING_UID = new ProfileTypeUID(SCOPE, "debounce-counting"); + public static final ProfileTypeUID DEBOUNCE_TIME_UID = new ProfileTypeUID(SCOPE, "debounce-time"); + public static final ProfileTypeUID INVERT_UID = new ProfileTypeUID(SCOPE, "invert"); + 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") // + .withSupportedItemTypes(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, CoreItemFactory.PLAYER, + CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH) // + .build(); + private static final ProfileType PROFILE_TYPE_GENERIC_TOGGLE_SWITCH = ProfileTypeBuilder + .newTrigger(GENERIC_TOGGLE_SWITCH_UID, "Generic Toggle Switch") // + .withSupportedItemTypes(CoreItemFactory.COLOR, CoreItemFactory.DIMMER, CoreItemFactory.SWITCH) // + .build(); + private static final ProfileType PROFILE_TYPE_DEBOUNCE_COUNTING = ProfileTypeBuilder + .newState(DEBOUNCE_COUNTING_UID, "Debounce (Counting)").build(); + private static final ProfileType PROFILE_TYPE_DEBOUNCE_TIME = ProfileTypeBuilder + .newState(DEBOUNCE_TIME_UID, "Debounce (Time)").build(); + private static final ProfileType PROFILE_TYPE_INVERT = ProfileTypeBuilder.newState(INVERT_UID, "Invert / Negate") + .withSupportedItemTypes(CoreItemFactory.CONTACT, CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, + CoreItemFactory.PLAYER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH) // + .withSupportedItemTypesOfChannel(CoreItemFactory.CONTACT, CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, + CoreItemFactory.PLAYER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH) // + .build(); + private static final ProfileType PROFILE_TYPE_ROUND = ProfileTypeBuilder.newState(ROUND_UID, "Round") + .withSupportedItemTypes(CoreItemFactory.NUMBER) // + .withSupportedItemTypesOfChannel(CoreItemFactory.NUMBER) // + .build(); + private static final ProfileType PROFILE_TYPE_THRESHOLD = ProfileTypeBuilder.newState(THRESHOLD_UID, "Threshold") // + .withSupportedItemTypesOfChannel(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER) // + .withSupportedItemTypes(CoreItemFactory.SWITCH) // + .build(); + private static final ProfileType PROFILE_TYPE_TIME_RANGE_COMMAND = ProfileTypeBuilder + .newState(TIME_RANGE_COMMAND_UID, "Time Range Command") // + .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, 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_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, @Reference ItemRegistry itemRegistry) { + this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService; + this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); + this.itemRegistry = itemRegistry; + } + + @Override + public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, + ProfileContext context) { + if (GENERIC_COMMAND_UID.equals(profileTypeUID)) { + return new GenericCommandTriggerProfile(callback, context); + } else if (GENERIC_TOGGLE_SWITCH_UID.equals(profileTypeUID)) { + return new GenericToggleSwitchTriggerProfile(callback, context); + } else if (DEBOUNCE_COUNTING_UID.equals(profileTypeUID)) { + return new DebounceCountingStateProfile(callback, context); + } else if (DEBOUNCE_TIME_UID.equals(profileTypeUID)) { + return new DebounceTimeStateProfile(callback, context); + } else if (INVERT_UID.equals(profileTypeUID)) { + return new InvertStateProfile(callback); + } else if (ROUND_UID.equals(profileTypeUID)) { + return new RoundStateProfile(callback, context); + } else if (THRESHOLD_UID.equals(profileTypeUID)) { + 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; + } + + @Override + public Collection getProfileTypes(@Nullable Locale locale) { + return SUPPORTED_PROFILE_TYPES.stream().map(p -> createLocalizedProfileType(p, locale)) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Collection getSupportedProfileTypeUIDs() { + return SUPPORTED_PROFILE_TYPE_UIDS; + } + + @Override + public @Nullable ProfileTypeUID getSuggestedProfileTypeUID(ChannelType channelType, @Nullable String itemType) { + return null; + } + + @Override + public @Nullable ProfileTypeUID getSuggestedProfileTypeUID(Channel channel, @Nullable String itemType) { + return null; + } + + private ProfileType createLocalizedProfileType(ProfileType profileType, @Nullable Locale locale) { + final LocalizedKey localizedKey = new LocalizedKey(profileType.getUID(), + locale != null ? locale.toLanguageTag() : null); + + final ProfileType cachedlocalizedProfileType = localizedProfileTypeCache.get(localizedKey); + if (cachedlocalizedProfileType != null) { + return cachedlocalizedProfileType; + } + + final ProfileType localizedProfileType = profileTypeI18nLocalizationService.createLocalizedProfileType(bundle, + profileType, locale); + if (localizedProfileType != null) { + localizedProfileTypeCache.put(localizedKey, localizedProfileType); + return localizedProfileType; + } else { + return profileType; + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/AbstractTriggerProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/AbstractTriggerProfile.java new file mode 100644 index 0000000000000..15f9fe004a0b9 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/AbstractTriggerProfile.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.TriggerProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AbstractTriggerProfile} class implements the behavior when being linked to an item. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractTriggerProfile implements TriggerProfile { + + private final Logger logger = LoggerFactory.getLogger(AbstractTriggerProfile.class); + + public static final String PARAM_EVENTS = "events"; + + final ProfileCallback callback; + final List events; + + @SuppressWarnings("unchecked") + AbstractTriggerProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + + Object paramValue = context.getConfiguration().get(PARAM_EVENTS); + logger.trace("Configuring profile '{}' with '{}' parameter: '{}'", getProfileTypeUID(), PARAM_EVENTS, + paramValue); + if (paramValue instanceof String) { + String event = paramValue.toString(); + events = Collections.unmodifiableList(Arrays.asList(event.split(","))); + } else if (paramValue instanceof Iterable) { + List values = new ArrayList<>(); + for (String event : (Iterable) paramValue) { + values.add(event); + } + events = Collections.unmodifiableList(values); + } else { + logger.error("Parameter '{}' is not a comma separated list of Strings", PARAM_EVENTS); + events = List.of(); + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfile.java new file mode 100644 index 0000000000000..2ae4975649383 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfile.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.DEBOUNCE_COUNTING_UID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +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.transform.basicprofiles.internal.config.DebounceCountingStateProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Debounces a {@link State} by counting. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class DebounceCountingStateProfile implements StateProfile { + + private final Logger logger = LoggerFactory.getLogger(DebounceCountingStateProfile.class); + + private final ProfileCallback callback; + + private final int numberOfChanges; + private int counter; + + private @Nullable State previousState; + + public DebounceCountingStateProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + DebounceCountingStateProfileConfig config = context.getConfiguration() + .as(DebounceCountingStateProfileConfig.class); + logger.debug("Configuring profile with parameters: [numberOfChanges='{}']", config.numberOfChanges); + + if (config.numberOfChanges < 0) { + throw new IllegalArgumentException(String + .format("numberOfChanges has to be a non-negative integer but was '%d'.", config.numberOfChanges)); + } + + this.numberOfChanges = config.numberOfChanges; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return DEBOUNCE_COUNTING_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + this.previousState = state; + } + + @Override + public void onCommandFromItem(Command command) { + // no-op + } + + @Override + public void onCommandFromHandler(Command command) { + // no-op + } + + @Override + public void onStateUpdateFromHandler(State state) { + logger.debug("Received state update from Handler"); + State localPreviousState = previousState; + if (localPreviousState == null) { + callback.sendUpdate(state); + previousState = state; + } else { + if (state.equals(localPreviousState.as(state.getClass()))) { + logger.debug("Item state back to previous state, reset counter"); + callback.sendUpdate(localPreviousState); + counter = 0; + } else { + logger.debug("Item state changed, counting"); + counter++; + if (numberOfChanges < counter) { + logger.debug("Item state has changed {} times, send new state to Item", counter); + callback.sendUpdate(state); + previousState = state; + counter = 0; + } else { + callback.sendUpdate(localPreviousState); + } + } + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceTimeStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceTimeStateProfile.java new file mode 100644 index 0000000000000..bc17217f5d8a6 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceTimeStateProfile.java @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.DEBOUNCE_TIME_UID; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +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.transform.basicprofiles.internal.config.DebounceTimeStateProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Debounces a {@link State} by time. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DebounceTimeStateProfile implements StateProfile { + private final Logger logger = LoggerFactory.getLogger(DebounceTimeStateProfile.class); + + private final ProfileCallback callback; + private final DebounceTimeStateProfileConfig config; + private final ScheduledExecutorService scheduler; + + private @Nullable ScheduledFuture toHandlerJob; + private @Nullable ScheduledFuture toItemJob; + + public DebounceTimeStateProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + this.scheduler = context.getExecutorService(); + this.config = context.getConfiguration().as(DebounceTimeStateProfileConfig.class); + logger.debug("Configuring profile with parameters: {}", config); + + if (config.toHandlerDelay < 0) { + throw new IllegalArgumentException(String + .format("toHandlerDelay has to be a non-negative integer but was '%d'.", config.toHandlerDelay)); + } + + if (config.toItemDelay < 0) { + throw new IllegalArgumentException( + String.format("toItemDelay has to be a non-negative integer but was '%d'.", config.toItemDelay)); + } + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return DEBOUNCE_TIME_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + // no-op + } + + @Override + public void onCommandFromItem(Command command) { + logger.debug("Received command '{}' from item", command); + if (config.toHandlerDelay == 0) { + callback.handleCommand(command); + return; + } + ScheduledFuture localToHandlerJob = toHandlerJob; + if (config.mode == DebounceTimeStateProfileConfig.DebounceMode.LAST) { + if (localToHandlerJob != null) { + // if we have an old job, cancel it + localToHandlerJob.cancel(true); + } + logger.trace("Scheduling command '{}'", command); + scheduleToHandler(() -> { + logger.debug("Sending command '{}' to handler", command); + callback.handleCommand(command); + }); + } else { + if (localToHandlerJob == null) { + // send the value only if we don't have a job + callback.handleCommand(command); + scheduleToHandler(null); + } else { + logger.trace("Discarding command to handler '{}'", command); + } + } + } + + private void scheduleToHandler(@Nullable Runnable function) { + toHandlerJob = scheduler.schedule(() -> { + if (function != null) { + function.run(); + } + toHandlerJob = null; + }, config.toHandlerDelay, TimeUnit.MILLISECONDS); + } + + @Override + public void onCommandFromHandler(Command command) { + logger.debug("Received command '{}' from handler", command); + if (config.toItemDelay == 0) { + callback.sendCommand(command); + return; + } + + ScheduledFuture localToItemJob = toItemJob; + if (config.mode == DebounceTimeStateProfileConfig.DebounceMode.LAST) { + if (localToItemJob != null) { + // if we have an old job, cancel it + localToItemJob.cancel(true); + } + logger.trace("Scheduling command '{}' to item", command); + scheduleToItem(() -> { + logger.debug("Sending command '{}' to item", command); + callback.sendCommand(command); + }); + } else { + if (localToItemJob == null) { + // only schedule a new job if we have none + callback.sendCommand(command); + scheduleToItem(null); + } else { + logger.trace("Discarding command to item '{}'", command); + } + } + } + + @Override + public void onStateUpdateFromHandler(State state) { + logger.debug("Received state update from Handler"); + if (config.toItemDelay == 0) { + callback.sendUpdate(state); + return; + } + ScheduledFuture localToItemJob = toItemJob; + if (config.mode == DebounceTimeStateProfileConfig.DebounceMode.LAST) { + if (localToItemJob != null) { + // if we have an old job, cancel it + localToItemJob.cancel(true); + } + logger.trace("Scheduling state update '{}' to item", state); + scheduleToItem(() -> { + logger.debug("Sending state update '{}' to item", state); + callback.sendUpdate(state); + }); + } else { + if (toItemJob == null) { + // only schedule a new job if we have none + callback.sendUpdate(state); + scheduleToItem(null); + } else { + logger.trace("Discarding state update to item '{}'", state); + } + } + } + + private void scheduleToItem(@Nullable Runnable function) { + toItemJob = scheduler.schedule(() -> { + if (function != null) { + function.run(); + } + toItemJob = null; + }, config.toItemDelay, TimeUnit.MILLISECONDS); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java new file mode 100644 index 0000000000000..6ab97c243c798 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.GENERIC_COMMAND_UID; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +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.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TypeParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link GenericCommandTriggerProfile} class implements the behavior when being linked to an item. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class GenericCommandTriggerProfile extends AbstractTriggerProfile { + + private final Logger logger = LoggerFactory.getLogger(GenericCommandTriggerProfile.class); + + private static final List> SUPPORTED_COMMANDS = List.of(IncreaseDecreaseType.class, + NextPreviousType.class, OnOffType.class, PlayPauseType.class, RewindFastforwardType.class, + StopMoveType.class, UpDownType.class); + + public static final String PARAM_COMMAND = "command"; + + private @Nullable Command command; + + public GenericCommandTriggerProfile(ProfileCallback callback, ProfileContext context) { + super(callback, context); + + Object paramValue = context.getConfiguration().get(PARAM_COMMAND); + logger.trace("Configuring profile '{}' with '{}' parameter: '{}'", getProfileTypeUID(), PARAM_COMMAND, + paramValue); + if (paramValue instanceof String) { + String value = (String) paramValue; + command = TypeParser.parseCommand(SUPPORTED_COMMANDS, value); + if (command == null) { + logger.debug("Value '{}' for parameter '{}' is a not supported command. Using StringType instead.", + value, PARAM_COMMAND); + command = StringType.valueOf(value); + } + } else { + logger.error("Parameter '{}' is not of type String: {}", PARAM_COMMAND, paramValue); + } + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return GENERIC_COMMAND_UID; + } + + @Override + public void onTriggerFromHandler(String payload) { + Command c = command; + if (c != null && events.contains(payload)) { + callback.sendCommand(c); + } + } + + @Override + public void onStateUpdateFromItem(State state) { + // do nothing + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfile.java new file mode 100644 index 0000000000000..5cc29cebd4e03 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfile.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.GENERIC_TOGGLE_SWITCH_UID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.OnOffType; +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.types.State; + +/** + * The {@link GenericToggleSwitchTriggerProfile} class implements the behavior when being linked to an item. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class GenericToggleSwitchTriggerProfile extends AbstractTriggerProfile { + + private @Nullable State previousState; + + public GenericToggleSwitchTriggerProfile(ProfileCallback callback, ProfileContext context) { + super(callback, context); + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return GENERIC_TOGGLE_SWITCH_UID; + } + + @Override + public void onTriggerFromHandler(String payload) { + if (events.contains(payload)) { + OnOffType state = OnOffType.ON.equals(previousState) ? OnOffType.OFF : OnOffType.ON; + callback.sendCommand(state); + previousState = state; + } + } + + @Override + public void onStateUpdateFromItem(State state) { + previousState = state.as(OnOffType.class); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java new file mode 100644 index 0000000000000..964551aaf1081 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.INVERT_UID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.profiles.ProfileCallback; +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.Type; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Inverts a {@link Command} or {@link State}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class InvertStateProfile implements StateProfile { + + private final Logger logger = LoggerFactory.getLogger(InvertStateProfile.class); + + private final ProfileCallback callback; + + public InvertStateProfile(ProfileCallback callback) { + this.callback = callback; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return INVERT_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + // do nothing + } + + @Override + public void onCommandFromItem(Command command) { + callback.handleCommand((Command) invert(command)); + } + + @Override + public void onCommandFromHandler(Command command) { + callback.sendCommand((Command) invert(command)); + } + + @Override + public void onStateUpdateFromHandler(State state) { + callback.sendUpdate((State) invert(state)); + } + + private Type invert(Type type) { + if (type instanceof UnDefType) { + // we cannot invert UNDEF or NULL values, thus we simply return them without reporting an error or warning + return type; + } + + if (type instanceof QuantityType) { + QuantityType qtState = (QuantityType) type; + return qtState.negate(); + } else if (type instanceof PercentType) { + PercentType ptState = (PercentType) type; + return new PercentType(100 - ptState.intValue()); + } else if (type instanceof DecimalType) { + DecimalType dtState = (DecimalType) type; + return new DecimalType(-1 * dtState.doubleValue()); + } else if (type instanceof IncreaseDecreaseType) { + return IncreaseDecreaseType.INCREASE.equals(type) ? IncreaseDecreaseType.DECREASE + : IncreaseDecreaseType.INCREASE; + } else if (type instanceof NextPreviousType) { + return NextPreviousType.NEXT.equals(type) ? NextPreviousType.PREVIOUS : NextPreviousType.NEXT; + } else if (type instanceof OnOffType) { + return OnOffType.ON.equals(type) ? OnOffType.OFF : OnOffType.ON; + } else if (type instanceof OpenClosedType) { + return OpenClosedType.OPEN.equals(type) ? OpenClosedType.CLOSED : OpenClosedType.OPEN; + } else if (type instanceof PlayPauseType) { + return PlayPauseType.PLAY.equals(type) ? PlayPauseType.PAUSE : PlayPauseType.PLAY; + } else if (type instanceof RewindFastforwardType) { + return RewindFastforwardType.REWIND.equals(type) ? RewindFastforwardType.FASTFORWARD + : RewindFastforwardType.REWIND; + } else if (type instanceof StopMoveType) { + return StopMoveType.MOVE.equals(type) ? StopMoveType.STOP : StopMoveType.MOVE; + } else if (type instanceof UpDownType) { + return UpDownType.UP.equals(type) ? UpDownType.DOWN : UpDownType.UP; + } else { + logger.warn("Invert cannot be applied to the type of class '{}'. Returning original type.", + type.getClass()); + return type; + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java new file mode 100644 index 0000000000000..2ed074ef39045 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.ROUND_UID; + +import java.math.RoundingMode; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; +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.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.Type; +import org.openhab.core.types.UnDefType; +import org.openhab.transform.basicprofiles.internal.config.RoundStateProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Applies rounding with the specified scale and the rounding mode to a {@link QuantityType} or {@link DecimalType} + * state. Default rounding mode is {@link RoundingMode#HALF_UP}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class RoundStateProfile implements StateProfile { + + private final Logger logger = LoggerFactory.getLogger(RoundStateProfile.class); + + public static final String PARAM_SCALE = "scale"; + public static final String PARAM_MODE = "mode"; + + private final ProfileCallback callback; + + final int scale; + final RoundingMode roundingMode; + + public RoundStateProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + RoundStateProfileConfig config = context.getConfiguration().as(RoundStateProfileConfig.class); + logger.debug("Configuring profile with parameters: [{scale='{}', mode='{}']", config.scale, config.mode); + + int localScale = 0; + Integer configScale = config.scale; + if (configScale instanceof Number) { + localScale = ((Number) configScale).intValue(); + } else { + logger.error("Parameter 'scale' is not of type String or Number."); + } + + RoundingMode localRoundingMode = RoundingMode.HALF_UP; + if (config.mode instanceof String) { + try { + localRoundingMode = RoundingMode.valueOf(config.mode); + } catch (IllegalArgumentException e) { + logger.warn("Parameter 'mode' is not a supported rounding mode: '{}'. Using default.", config.mode); + } + } else { + logger.error("Parameter 'mode' is not of type String."); + } + + this.scale = localScale; + this.roundingMode = localRoundingMode; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return ROUND_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + // do nothing + } + + @Override + public void onCommandFromItem(Command command) { + callback.handleCommand((Command) applyRound(command)); + } + + @Override + public void onCommandFromHandler(Command command) { + callback.sendCommand((Command) applyRound(command)); + } + + @Override + public void onStateUpdateFromHandler(State state) { + callback.sendUpdate((State) applyRound(state)); + } + + private Type applyRound(Type state) { + if (state instanceof UnDefType) { + // we cannot round UNDEF or NULL values, thus we simply return them without reporting an error or warning + return state; + } + + Type result = UnDefType.UNDEF; + if (state instanceof QuantityType) { + QuantityType qtState = (QuantityType) state; + result = new QuantityType<>(qtState.toBigDecimal().setScale(scale, roundingMode), qtState.getUnit()); + } else if (state instanceof DecimalType) { + DecimalType dtState = (DecimalType) state; + result = new DecimalType(dtState.toBigDecimal().setScale(scale, roundingMode)); + } else { + logger.warn( + "Round cannot be applied to the incompatible state '{}' sent from the binding. Returning original state.", + state); + result = state; + } + return result; + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java new file mode 100644 index 0000000000000..7e0c8f9947217 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.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.openhab.transform.basicprofiles.internal.config.StateFilterProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 = List.of(); + + private @Nullable 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 = parseConditions(config.conditions, config.separator); + configMismatchState = parseState(config.mismatchState); + } + } + + private List parseConditions(@Nullable String config, String separator) { + if (config == null) { + return List.of(); + } + + List parsedConditions = new ArrayList<>(); + try { + String[] expressions = config.split(separator); + for (String expression : expressions) { + String[] parts = expression.trim().split("\s"); + if (parts.length == 3) { + String itemName = parts[0]; + StateCondition.ComparisonType conditionType = StateCondition.ComparisonType + .valueOf(parts[1].toUpperCase(Locale.ROOT)); + String value = parts[2]; + parsedConditions.add(new StateCondition(itemName, conditionType, value)); + } else { + logger.warn("Malformed condition expression: '{}'", expression); + } + } + + 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) { + 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); + } + } + + @Nullable + private State checkCondition(State state) { + if (!conditions.isEmpty()) { + boolean allConditionsMet = true; + for (StateCondition condition : conditions) { + logger.debug("Evaluting condition: {}", condition); + try { + Item item = itemRegistry.getItem(condition.itemName); + String itemState = item.getState().toString(); + + if (!condition.matches(itemState)) { + 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 StateCondition { + String itemName; + + ComparisonType comparisonType; + String value; + + boolean quoted = false; + + public StateCondition(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: + return state.equals(value); + case NEQ: { + return !state.equals(value); + } + default: + logger.warn("Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update", + comparisonType); + return false; + + } + } + + enum ComparisonType { + EQ, + NEQ + } + + @Override + public String toString() { + return "Condition{itemName='" + itemName + "', comparisonType=" + comparisonType + ", value='" + value + + "'}'"; + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfile.java new file mode 100644 index 0000000000000..9601530faefeb --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfile.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.THRESHOLD_UID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +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.Type; +import org.openhab.transform.basicprofiles.internal.config.ThresholdStateProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/*** + * This is the default implementation for a {@link ThresholdStateProfile}}. Triggers ON/OFF behavior when being linked + * to a Switch item if value is below threshold (default: 10). + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class ThresholdStateProfile implements StateProfile { + + private final Logger logger = LoggerFactory.getLogger(ThresholdStateProfile.class); + + public static final String PARAM_THRESHOLD = "threshold"; + + private final ProfileCallback callback; + private final ThresholdStateProfileConfig config; + + public ThresholdStateProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + this.config = context.getConfiguration().as(ThresholdStateProfileConfig.class); + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return THRESHOLD_UID; + } + + @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); + 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); + callback.sendUpdate((State) mappedState); + } + + private Type mapValue(Type value) { + if (value instanceof Number) { + return ((Number) value).intValue() <= config.threshold ? OnOffType.ON : OnOffType.OFF; + } + return OnOffType.OFF; + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java new file mode 100644 index 0000000000000..e25fd3bac174c --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.TIME_RANGE_COMMAND_UID; + +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +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.transform.basicprofiles.internal.config.TimeRangeCommandProfileConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an enhanced implementation of a follow profile which converts {@link OnOffType} to a {@link PercentType}. + * The value of the percent type can be different between a specific time of the day. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class TimeRangeCommandProfile implements StateProfile { + + private static final Pattern HHMM_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3])(:[0-5][0-9])$"); + private static final String TIME_SEPARATOR = ":"; + + private static final String PARAM_IN_RANGE_VALUE = "inRangeValue"; + private static final String PARAM_OUT_OF_RANGE_VALUE = "outOfRangeValue"; + public static final String PARAM_START = "start"; + public static final String PARAM_END = "end"; + + public static final String CONFIG_RESTORE_VALUE_OFF = "OFF"; + private static final String CONFIG_RESTORE_VALUE_PREVIOUS = "PREVIOUS"; + private static final String CONFIG_RESTORE_VALUE_NOTHING = "NOTHING"; + + private final Logger logger = LoggerFactory.getLogger(TimeRangeCommandProfile.class); + private final ProfileCallback callback; + + private final PercentType inRangeValue; + private final PercentType outOfRangeValue; + private final long startTimeInMinutes; + private final long endTimeInMinutes; + private final String restoreValue; + + private @Nullable PercentType previousState; + private @Nullable PercentType restoreState; + + public TimeRangeCommandProfile(ProfileCallback callback, ProfileContext context) { + this.callback = callback; + TimeRangeCommandProfileConfig config = context.getConfiguration().as(TimeRangeCommandProfileConfig.class); + logger.debug( + "Configuring profile with parameters: [{inRangeValue='{}', outOfRangeValue='{}', start='{}', end='{}', restoreValue='{}']", + config.inRangeValue, config.outOfRangeValue, config.start, config.end, config.restoreValue); + + inRangeValue = parsePercentTypeConfigValue(config.inRangeValue, PARAM_IN_RANGE_VALUE); + outOfRangeValue = parsePercentTypeConfigValue(config.outOfRangeValue, PARAM_OUT_OF_RANGE_VALUE); + + startTimeInMinutes = parseTimeConfigValue(config.start, PARAM_START); + endTimeInMinutes = parseTimeConfigValue(config.end, PARAM_END); + // We have to do this here, otherwise the comparison in getOnValue() does not work. A range beyond midnight + // (e.g. start="23:00", end="01:00") is not yet supported. + if (endTimeInMinutes <= startTimeInMinutes) { + logger.warn("Parameter '{}' ({}) is earlier than or equal to parameter '{}' ({}).", PARAM_END, config.end, + PARAM_START, config.start); + throw new IllegalArgumentException("Invalid time range parameter"); + } + + restoreValue = config.restoreValue; + } + + private PercentType parsePercentTypeConfigValue(int value, String field) { + try { + return new PercentType(value); + } catch (IllegalArgumentException e) { + logger.warn("Cannot parse profile configuration '{}' to percent, use a value between 0 and 100!", field); + throw new IllegalArgumentException("Invalid profile configuration '" + field + "'", e); + } + } + + private long parseTimeConfigValue(String value, String field) { + try { + return getMinutesFromTime(value); + } catch (PatternSyntaxException | NumberFormatException | ArithmeticException e) { + logger.warn("Cannot parse profile configuration '{}' to hour and minutes, use pattern hh:mm!", field); + throw new IllegalArgumentException("Invalid profile configuration '" + field + "'", e); + } + } + + /** + * Parses a hh:mm string and returns the minutes. + */ + private long getMinutesFromTime(String configTime) + throws PatternSyntaxException, NumberFormatException, ArithmeticException { + String time = configTime; + if (!(time = time.trim()).isBlank()) { + if (!HHMM_PATTERN.matcher(time).matches()) { + throw new NumberFormatException(); + } else { + String[] splittedConfigTime = time.split(TIME_SEPARATOR); + if (splittedConfigTime.length < 2) { + throw new NumberFormatException(); + } + int hour = Integer.parseInt(splittedConfigTime[0]); + int minutes = Integer.parseInt(splittedConfigTime[1]); + return Duration.ofMinutes(minutes).plusHours(hour).toMinutes(); + } + } + return 0; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return TIME_RANGE_COMMAND_UID; + } + + @Override + public void onStateUpdateFromItem(State state) { + if (!(state instanceof OnOffType)) { + logger.debug("The given state '{}' cannot be transformed to an OnOffType.", state); + return; + } + + PercentType newCommand = OnOffType.OFF.equals(state) ? getOffValue() : getOnValue(); + if (newCommand != null) { + logger.debug("Forward new command '{}' to the respective thing handler.", newCommand); + if (CONFIG_RESTORE_VALUE_PREVIOUS.equals(restoreValue)) { + logger.debug("'restoreValue' ({}): Remember previous state '{}'.", restoreValue, previousState); + restoreState = previousState; + } + callback.handleCommand(newCommand); + } + } + + @Override + public void onCommandFromHandler(Command command) { + if (!(command instanceof OnOffType)) { + logger.debug("The given command '{}' cannot be handled. It is not an OnOffType.", command); + return; + } + + PercentType newCommand = OnOffType.OFF.equals(command) ? getOffValue() : getOnValue(); + if (newCommand != null) { + logger.debug("Send new command '{}' to the framework.", newCommand); + if (CONFIG_RESTORE_VALUE_PREVIOUS.equals(restoreValue)) { + logger.debug("'restoreValue' ({}): Remember previous state '{}'.", restoreValue, previousState); + restoreState = previousState; + } + callback.sendCommand(newCommand); + } + } + + private @Nullable PercentType getOffValue() { + switch (restoreValue) { + case CONFIG_RESTORE_VALUE_OFF: + return PercentType.ZERO; + case CONFIG_RESTORE_VALUE_NOTHING: + logger.debug("'restoreValue' ({}): Do nothing.", restoreValue); + return null; + case CONFIG_RESTORE_VALUE_PREVIOUS: + return restoreState; + default: + // try to transform config parameter 'restoreValue' to a valid PercentType + try { + return PercentType.valueOf(restoreValue); + } catch (IllegalArgumentException e) { + logger.warn("The given parameter 'restoreValue' ({}) cannot be transformed to a valid PercentType.", + restoreValue); + return null; + } + } + } + + private PercentType getOnValue() { + ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()); + ZonedDateTime today = now.truncatedTo(ChronoUnit.DAYS); + return now.isAfter(today.plusMinutes(startTimeInMinutes)) && now.isBefore(today.plusMinutes(endTimeInMinutes)) + ? inRangeValue + : outOfRangeValue; + } + + @Override + public void onCommandFromItem(Command command) { + // no-op + } + + @Override + public void onStateUpdateFromHandler(State state) { + PercentType pState = state.as(PercentType.class); + if (pState != null) { + logger.debug("Item state changed, set 'previousState' to '{}'.", pState); + previousState = pState; + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..a1754d3f64070 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + transformation + Basic Profiles + A set of profiles with basic functionality. + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/debounce.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/debounce.xml new file mode 100644 index 0000000000000..e87f6ca53d10f --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/debounce.xml @@ -0,0 +1,36 @@ + + + + + + + Number of changes before updating Item State. + 1 + + + + + + + Timespan before an value is forwarded to the item (or discarded after the first value). + 0 + + + + Timespan before an value is forwarded to the handler (or discarded after the first value). + 0 + + + + + + + + LAST + true + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/generic-command.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/generic-command.xml new file mode 100644 index 0000000000000..41b6f00afb360 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/generic-command.xml @@ -0,0 +1,34 @@ + + + + + + + Comma separated list of events to which the profile should listen. + + + + Command which should be sent if the event is triggered. + false + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/round.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/round.xml new file mode 100644 index 0000000000000..5f9d35c73f872 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/round.xml @@ -0,0 +1,27 @@ + + + + + + + Scale to indicate the resulting number of decimal places. + + + + Rounding mode to be used (e.g. "UP", "DOWN", "CEILING", "FLOOR", "HALF_UP" or "HALF_DOWN"). + + HALF_UP + + + + + + + + + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml new file mode 100644 index 0000000000000..c468dcd9fdd1a --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml @@ -0,0 +1,18 @@ + + + + + + + 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` + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/threshold.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/threshold.xml new file mode 100644 index 0000000000000..28eebbd404c16 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/threshold.xml @@ -0,0 +1,14 @@ + + + + + + + Triggers ON if value is below the given threshold, otherwise OFF. + 10 + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/time-range-command.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/time-range-command.xml new file mode 100644 index 0000000000000..6c0826a395cac --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/time-range-command.xml @@ -0,0 +1,43 @@ + + + + + + + The value which will be send when the profile detects ON and current time is between start time and end + time. + 100 + + + + The value which will be send when the profile detects ON and current time is NOT between start time and + end time. + 30 + + + + The start time of the day (hh:mm). + time + + + + The end time of the day (hh:mm). + time + + + true + + Select what should happen when the profile detects OFF again. + + + + + + false + OFF + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/toggle-switch.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/toggle-switch.xml new file mode 100644 index 0000000000000..58c1120f25b75 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/config/toggle-switch.xml @@ -0,0 +1,13 @@ + + + + + + + Comma separated list of events to which the profile should listen. + + + diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles.properties b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles.properties new file mode 100644 index 0000000000000..0b287c6c11cdb --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles.properties @@ -0,0 +1,49 @@ +profile-type.basic-profiles.generic-command.label = Generic Command +profile.config.basic-profiles.generic-command.events.label = Events +profile.config.basic-profiles.generic-command.events.description = Comma separated list of events to which the profile should listen. +profile.config.basic-profiles.generic-command.command.label = Command +profile.config.basic-profiles.generic-command.command.description = Command which should be sent if the event is triggered. + +profile-type.basic-profiles.toggle-switch.label = Generic Toggle Switch +profile.config.basic-profiles.toggle-switch.events.label = Events +profile.config.basic-profiles.toggle-switch.events.description = Comma separated list of events to which the profile should listen. + +profile-type.basic-profiles.debounce-counting.label = Debounce (Counting) +profile.config.basic-profiles.debounce-counting.numberOfChanges.label = Number Of Changes +profile.config.basic-profiles.debounce-counting.numberOfChanges.description = Number of changes before updating Item State. + +profile-type.basic-profiles.debounce-time.label = Debounce (Time) +profile.config.basic-profiles.debounce-time.toItemDelay.label = To Item Delay +profile.config.basic-profiles.debounce-time.toItemDelay.description = Milliseconds before updating Item State. +profile.config.basic-profiles.debounce-time.toHandlerDelay.label = To Handler Delay +profile.config.basic-profiles.debounce-time.toHandlerDelay.description = Milliseconds before sending to thing handler. +profile.config.basic-profiles.debounce-time.mode.label = Mode +profile.config.basic-profiles.debounce-time.mode.option.FIRST = Send first value +profile.config.basic-profiles.debounce-time.mode.option.LAST = Send last value + +profile-type.basic-profiles.invert.label = Invert / Negate + +profile-type.basic-profiles.round.label = Round +profile.config.basic-profiles.round.scale.label = Scale +profile.config.basic-profiles.round.scale.description = Scale to indicate the resulting number of decimal places. +profile.config.basic-profiles.round.mode.label = Rounding Mode +profile.config.basic-profiles.round.mode.description = Rounding mode to be used (UP, DOWN, CEILING, FLOOR, HALF_UP or HALF_DOWN). + +profile-type.basic-profiles.threshold.label = Threshold +profile.config.basic-profiles.threshold.threshold.label = Threshold +profile.config.basic-profiles.threshold.threshold.description = Triggers ON if value is below the given threshold, otherwise OFF. + +profile-type.basic-profiles.time-range-command.label = Time Range Command +profile.config.basic-profiles.time-range-command.inRangeValue.label = In Range Value +profile.config.basic-profiles.time-range-command.inRangeValue.description = The value which will be send when the profile detects ON and current time is between start time and end time. +profile.config.basic-profiles.time-range-command.outOfRangeValue.label = Out Of Range Value +profile.config.basic-profiles.time-range-command.outOfRangeValue.description = The value which will be send when the profile detects ON and current time is NOT between start time and end time. +profile.config.basic-profiles.time-range-command.start.label = Start Time +profile.config.basic-profiles.time-range-command.start.description = The start time of the day (hh:mm). +profile.config.basic-profiles.time-range-command.end.label = End Time +profile.config.basic-profiles.time-range-command.end.description = The end time of the day (hh:mm). +profile.config.basic-profiles.time-range-command.restoreValue.label = Restore Value +profile.config.basic-profiles.time-range-command.restoreValue.description = Select what should happen when the profile detects OFF again. +profile.config.basic-profiles.time-range-command.restoreValue.option.OFF = Off +profile.config.basic-profiles.time-range-command.restoreValue.option.PREVIOUS = Return to previous value +profile.config.basic-profiles.time-range-command.restoreValue.option.NOTHING = Do nothing diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties new file mode 100644 index 0000000000000..e31d8339b0d58 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties @@ -0,0 +1,49 @@ +profile-type.basic-profiles.generic-command.label = Generic Command +profile.config.basic-profiles.generic-command.events.label = Ereignisse +profile.config.basic-profiles.generic-command.events.description = Kommagetrennte Liste der Ereignisse, die das Profil verfolgen soll. +profile.config.basic-profiles.generic-command.command.label = Kommando +profile.config.basic-profiles.generic-command.command.description = Kommando, welches gesendet wird, sobald eines der konfigurierten Ereignisse eintritt. + +profile-type.basic-profiles.toggle-switch.label = Generic Toggle Switch +profile.config.basic-profiles.toggle-switch.events.label = Ereignisse +profile.config.basic-profiles.toggle-switch.events.description = Kommagetrennte Liste der Ereignisse, die das Profil verfolgen soll. + +profile-type.basic-profiles.debounce-counting.label = Entprellen (Zähler) +profile.config.basic-profiles.debounce-counting.numberOfChanges.label = Anzahl der Änderungen +profile.config.basic-profiles.debounce-counting.numberOfChanges.description = Anzahl der Änderungen bevor der Item State aktualisiert wird. + +profile-type.basic-profiles.debounce-time.label = Entprellen (Zeit) +profile.config.basic-profiles.debounce-time.toItemDelay.label = Verzögerung Zum Item +profile.config.basic-profiles.debounce-time.toItemDelay.description = Millisekunden bevor das Item Update gesendet wird. +profile.config.basic-profiles.debounce-time.toHandlerDelay.label = Verzögerung Zum Handler +profile.config.basic-profiles.debounce-time.toHandlerDelay.description = Millisekunden bevor das Update an den Handler gesendet wird. +profile.config.basic-profiles.debounce-time.mode.label = Modus +profile.config.basic-profiles.debounce-time.mode.option.FIRST = Sende ersten Wert +profile.config.basic-profiles.debounce-time.mode.option.LAST = Sende letzten Wert + +profile-type.basic-profiles.invert.label = Invertieren / Negieren + +profile-type.basic-profiles.round.label = Runden +profile.config.basic-profiles.round.scale.label = Skalierung +profile.config.basic-profiles.round.scale.description = Skalierung, die auf den Wert angewendet werden soll. +profile.config.basic-profiles.round.mode.label = Rundungsmodus +profile.config.basic-profiles.round.mode.description = Rundungsmodus, der verwendet werden soll (UP, DOWN, CEILING, FLOOR, HALF_UP oder HALF_DOWN). + +profile-type.basic-profiles.threshold.label = Grenzwert +profile.config.basic-profiles.threshold.threshold.label = Grenzwert +profile.config.basic-profiles.threshold.threshold.description = Bildet bei Unterschreitung des Grenzwertes (Vorgabe: 10) ON auf ein Switch Item ab, OFF sonst. + +profile-type.basic-profiles.time-range-command.label = Wert In Zeitspanne +profile.config.basic-profiles.time-range-command.inRangeValue.label = Wert Innerhalb Der Zeitspanne +profile.config.basic-profiles.time-range-command.inRangeValue.description = Der Wert, der gesendet wird, wenn das Profil ON erkennt und die aktuelle Zeit zwischen der Startzeit und der Endzeit liegt. +profile.config.basic-profiles.time-range-command.outOfRangeValue.label = Wert Außerhalb Der Zeitspanne +profile.config.basic-profiles.time-range-command.outOfRangeValue.description = Der Wert, der gesendet wird, wenn das Profil ON erkennt und die aktuelle Zeit NICHT zwischen der Startzeit und der Endzeit liegt. +profile.config.basic-profiles.time-range-command.start.label = Startzeit +profile.config.basic-profiles.time-range-command.start.description = Die Startzeit am Tag (hh:mm). +profile.config.basic-profiles.time-range-command.end.label = Endzeit +profile.config.basic-profiles.time-range-command.end.description = Die Endzeit am Tag (hh:mm). +profile.config.basic-profiles.time-range-command.restoreValue.label = Wert Wiederherstellen +profile.config.basic-profiles.time-range-command.restoreValue.description = Auswählen, was passieren soll, wenn das Profil wieder OFF erkennt. +profile.config.basic-profiles.time-range-command.restoreValue.option.OFF = Aus +profile.config.basic-profiles.time-range-command.restoreValue.option.PREVIOUS = Zurück zum vorherigen Status +profile.config.basic-profiles.time-range-command.restoreValue.option.NOTHING = Unternehme nichts diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java new file mode 100644 index 0000000000000..0b1127f785def --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.factory; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +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; +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.ItemRegistry; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.ProfileType; +import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService; +import org.openhab.core.util.BundleResolver; +import org.openhab.transform.basicprofiles.internal.profiles.GenericCommandTriggerProfile; +import org.openhab.transform.basicprofiles.internal.profiles.RoundStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.ThresholdStateProfile; +import org.openhab.transform.basicprofiles.internal.profiles.TimeRangeCommandProfile; + +/** + * Basic unit tests for {@link BasicProfilesFactory}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class BasicProfilesFactoryTest { + + 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", + GenericCommandTriggerProfile.PARAM_COMMAND, OnOffType.ON.toString(), TimeRangeCommandProfile.PARAM_START, + "08:00", TimeRangeCommandProfile.PARAM_END, "23:00"); + private static final Configuration CONFIG = new Configuration(PROPERTIES); + + private @Mock @NonNullByDefault({}) ProfileTypeI18nLocalizationService mockLocalizationService; + 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, mockItemRegistry); + + when(mockContext.getConfiguration()).thenReturn(CONFIG); + } + + @Test + public void systemProfileTypesAndUidsShouldBeAvailable() { + Collection supportedProfileTypeUIDs = profileFactory.getSupportedProfileTypeUIDs(); + assertThat(supportedProfileTypeUIDs, hasSize(NUMBER_OF_PROFILES)); + + Collection supportedProfileTypes = profileFactory.getProfileTypes(null); + assertThat(supportedProfileTypeUIDs, hasSize(NUMBER_OF_PROFILES)); + + for (ProfileType profileType : supportedProfileTypes) { + assertTrue(supportedProfileTypeUIDs.contains(profileType.getUID())); + } + } + + @Test + public void testFactoryCreatesAvailableProfiles() { + for (ProfileTypeUID profileTypeUID : profileFactory.getSupportedProfileTypeUIDs()) { + assertNotNull(profileFactory.createProfile(profileTypeUID, mockCallback, mockContext)); + } + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfileTest.java new file mode 100644 index 0000000000000..330efe3ab3450 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/DebounceCountingStateProfileTest.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +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.State; + +/** + * Debounces a {@link State} by counting. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +class DebounceCountingStateProfileTest { + + public static class ParameterSet { + public final List sourceStates; + public final List resultingStates; + public final int numberOfChanges; + + public ParameterSet(List sourceStates, List resultingStates, int numberOfChanges) { + this.sourceStates = sourceStates; + this.resultingStates = resultingStates; + this.numberOfChanges = numberOfChanges; + } + } + + public static Collection parameters() { + return Arrays.asList(new Object[][] { // + { new ParameterSet(List.of(OnOffType.ON), List.of(OnOffType.ON), 0) }, // + { new ParameterSet(List.of(OnOffType.ON), List.of(OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON), List.of(OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF), List.of(OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.OFF), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.OFF), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.OFF), 1) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.ON, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.ON, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF, OnOffType.ON), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.ON), 2) }, // + { new ParameterSet(List.of(OnOffType.ON, OnOffType.OFF, OnOffType.OFF, OnOffType.OFF), + List.of(OnOffType.ON, OnOffType.ON, OnOffType.ON, OnOffType.OFF), 2) } // + }); + } + + private @NonNullByDefault({}) @Mock ProfileCallback mockCallback; + private @NonNullByDefault({}) @Mock ProfileContext mockContext; + + @Test + public void testWrongParameterLower() { + assertThrows(IllegalArgumentException.class, () -> initProfile(-1)); + } + + @ParameterizedTest + @MethodSource("parameters") + public void testOnStateUpdateFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(parameterSet.numberOfChanges); + for (int i = 0; i < parameterSet.sourceStates.size(); i++) { + verifySendUpdate(profile, parameterSet.sourceStates.get(i), parameterSet.resultingStates.get(i)); + } + } + + private StateProfile initProfile(int numberOfChanges) { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("numberOfChanges", numberOfChanges))); + return new DebounceCountingStateProfile(mockCallback, mockContext); + } + + private void verifySendUpdate(StateProfile profile, State state, State expectedState) { + reset(mockCallback); + profile.onStateUpdateFromHandler(state); + verify(mockCallback, times(1)).sendUpdate(eq(expectedState)); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfileTest.java new file mode 100644 index 0000000000000..2c31311d395eb --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfileTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +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.library.types.OnOffType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.CommonTriggerEvents; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.TriggerProfile; +import org.openhab.core.types.Command; + +/** + * Basic unit tests for {@link GenericCommandTriggerProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class GenericCommandTriggerProfileTest { + + public static class ParameterSet { + public final String events; + public final Command command; + + public ParameterSet(String events, Command command) { + this.events = events; + this.command = command; + } + } + + public static Collection parameters() { + return List.of(new Object[][] { // + { new ParameterSet("1002", OnOffType.ON) }, // + { new ParameterSet("1002", OnOffType.OFF) }, // + { new ParameterSet("1002,1003", PlayPauseType.PLAY) }, // + { new ParameterSet("1002,1003", PlayPauseType.PAUSE) }, // + { new ParameterSet("1002,1003,3001", StopMoveType.STOP) }, // + { new ParameterSet("1002,1003,3001", StopMoveType.MOVE) }, // + { new ParameterSet(CommonTriggerEvents.LONG_PRESSED + "," + CommonTriggerEvents.SHORT_PRESSED, + UpDownType.UP) }, // + { new ParameterSet(CommonTriggerEvents.LONG_PRESSED + "," + CommonTriggerEvents.SHORT_PRESSED, + UpDownType.DOWN) }, // + { new ParameterSet("1003", StringType.valueOf("SELECT")) } // + }); + } + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + private @Mock @NonNullByDefault({}) ProfileContext mockContext; + + @ParameterizedTest + @MethodSource("parameters") + public void testOnOffSwitchItem(ParameterSet parameterSet) { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of(AbstractTriggerProfile.PARAM_EVENTS, + parameterSet.events, GenericCommandTriggerProfile.PARAM_COMMAND, parameterSet.command.toFullString()))); + + TriggerProfile profile = new GenericCommandTriggerProfile(mockCallback, mockContext); + + verifyNoAction(profile, CommonTriggerEvents.PRESSED, parameterSet.command); + for (String event : parameterSet.events.split(",")) { + verifyAction(profile, event, parameterSet.command); + } + } + + private void verifyAction(TriggerProfile profile, String trigger, Command expectation) { + reset(mockCallback); + profile.onTriggerFromHandler(trigger); + verify(mockCallback, times(1)).sendCommand(eq(expectation)); + } + + private void verifyNoAction(TriggerProfile profile, String trigger, Command expectation) { + reset(mockCallback); + profile.onTriggerFromHandler(trigger); + verify(mockCallback, times(0)).sendCommand(eq(expectation)); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfileTest.java new file mode 100644 index 0000000000000..616ec5b43c367 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/GenericToggleSwitchTriggerProfileTest.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +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.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.CommonTriggerEvents; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.TriggerProfile; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Basic unit tests for {@link GenericToggleSwitchTriggerProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class GenericToggleSwitchTriggerProfileTest { + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + private @Mock @NonNullByDefault({}) ProfileContext mockContext; + + @BeforeEach + public void setup() { + when(mockContext.getConfiguration()).thenReturn( + new Configuration(Map.of(AbstractTriggerProfile.PARAM_EVENTS, CommonTriggerEvents.PRESSED))); + } + + @Test + public void testSwitchItem() { + TriggerProfile profile = new GenericToggleSwitchTriggerProfile(mockCallback, mockContext); + + verifyAction(profile, UnDefType.NULL, OnOffType.ON); + verifyAction(profile, OnOffType.ON, OnOffType.OFF); + verifyAction(profile, OnOffType.OFF, OnOffType.ON); + } + + @Test + public void testDimmerItem() { + TriggerProfile profile = new GenericToggleSwitchTriggerProfile(mockCallback, mockContext); + + verifyAction(profile, UnDefType.NULL, OnOffType.ON); + verifyAction(profile, PercentType.HUNDRED, OnOffType.OFF); + verifyAction(profile, PercentType.ZERO, OnOffType.ON); + verifyAction(profile, new PercentType(50), OnOffType.OFF); + } + + @Test + public void testColorItem() { + TriggerProfile profile = new GenericToggleSwitchTriggerProfile(mockCallback, mockContext); + + verifyAction(profile, UnDefType.NULL, OnOffType.ON); + verifyAction(profile, HSBType.WHITE, OnOffType.OFF); + verifyAction(profile, HSBType.BLACK, OnOffType.ON); + verifyAction(profile, new HSBType("0,50,50"), OnOffType.OFF); + } + + private void verifyAction(TriggerProfile profile, State preCondition, Command expectation) { + reset(mockCallback); + profile.onStateUpdateFromItem(preCondition); + profile.onTriggerFromHandler(CommonTriggerEvents.PRESSED); + verify(mockCallback, times(1)).sendCommand(eq(expectation)); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfileTest.java new file mode 100644 index 0000000000000..59b94a4781822 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfileTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +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.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +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.library.unit.Units; +import org.openhab.core.thing.profiles.ProfileCallback; +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 InvertStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class InvertStateProfileTest { + + private static final DateTimeType NOW = new DateTimeType(); + + public static class ParameterSet { + public Type type; + public Type resultingType; + + public ParameterSet(Type source, Type result) { + this.type = source; + this.resultingType = result; + } + } + + public static Collection parameters() { + return List.of(new Object[][] { // + { new ParameterSet(UnDefType.NULL, UnDefType.NULL) }, // + { new ParameterSet(UnDefType.UNDEF, UnDefType.UNDEF) }, // + { new ParameterSet(new QuantityType<>(25, Units.LITRE), new QuantityType<>(-25, Units.LITRE)) }, // + { new ParameterSet(PercentType.ZERO, PercentType.HUNDRED) }, // + { new ParameterSet(PercentType.HUNDRED, PercentType.ZERO) }, // + { new ParameterSet(new PercentType(25), new PercentType(75)) }, // + { new ParameterSet(new DecimalType(25L), new DecimalType(-25L)) }, // + { new ParameterSet(OnOffType.ON, OnOffType.OFF) }, // + { new ParameterSet(OnOffType.OFF, OnOffType.ON) }, // + { new ParameterSet(NOW, NOW) } // + }); + } + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + + @ParameterizedTest + @MethodSource("parameters") + public void testOnCommandFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(); + if (parameterSet.type instanceof Command && parameterSet.resultingType instanceof Command) { + verifyCommandFromItem(profile, (Command) parameterSet.type, (Command) parameterSet.resultingType); + verifyCommandFromHandler(profile, (Command) parameterSet.type, (Command) parameterSet.resultingType); + } + } + + @ParameterizedTest + @MethodSource("parameters") + public void testOnStateUpdateFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(); + if (parameterSet.type instanceof State && parameterSet.resultingType instanceof State) { + verifyStateUpdateFromHandler(profile, (State) parameterSet.type, (State) parameterSet.resultingType); + } + } + + private StateProfile initProfile() { + return new InvertStateProfile(mockCallback); + } + + private void verifyCommandFromItem(StateProfile profile, Command command, Command result) { + reset(mockCallback); + profile.onCommandFromItem(command); + verify(mockCallback, times(1)).handleCommand(eq(result)); + } + + private void verifyCommandFromHandler(StateProfile profile, Command command, Command result) { + reset(mockCallback); + profile.onCommandFromHandler(command); + verify(mockCallback, times(1)).sendCommand(eq(result)); + } + + private void verifyStateUpdateFromHandler(StateProfile profile, State state, State result) { + reset(mockCallback); + profile.onStateUpdateFromHandler(state); + verify(mockCallback, times(1)).sendUpdate(eq(result)); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfileTest.java new file mode 100644 index 0000000000000..5a359de7adfea --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfileTest.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.*; + +import java.math.RoundingMode; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.types.Command; + +/** + * Basic unit tests for {@link RoundStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@NonNullByDefault +public class RoundStateProfileTest { + + @BeforeEach + public void setup() { + // initialize parser with ImperialUnits, otherwise units like °F are unknown + @SuppressWarnings("unused") + Unit fahrenheit = ImperialUnits.FAHRENHEIT; + } + + @Test + public void testParsingParameters() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, 2, "NOT_SUPPORTED"); + + assertThat(roundProfile.scale, is(2)); + assertThat(roundProfile.roundingMode, is(RoundingMode.HALF_UP)); + } + + @Test + public void testDecimalTypeOnCommandFromItem() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, 2); + + Command cmd = new DecimalType(23.333); + roundProfile.onCommandFromItem(cmd); + + ArgumentCaptor capture = ArgumentCaptor.forClass(Command.class); + verify(callback, times(1)).handleCommand(capture.capture()); + + Command result = capture.getValue(); + DecimalType dtResult = (DecimalType) result; + assertThat(dtResult.doubleValue(), is(23.33)); + } + + @Test + public void testDecimalTypeOnCommandFromItemWithNegativeScale() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, -2); + + Command cmd = new DecimalType(1234.333); + roundProfile.onCommandFromItem(cmd); + + ArgumentCaptor capture = ArgumentCaptor.forClass(Command.class); + verify(callback, times(1)).handleCommand(capture.capture()); + + Command result = capture.getValue(); + DecimalType dtResult = (DecimalType) result; + assertThat(dtResult.doubleValue(), is(1200.0)); + } + + @Test + public void testDecimalTypeOnCommandFromItemWithCeiling() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, 0, RoundingMode.CEILING.name()); + + Command cmd = new DecimalType(23.3); + roundProfile.onCommandFromItem(cmd); + + ArgumentCaptor capture = ArgumentCaptor.forClass(Command.class); + verify(callback, times(1)).handleCommand(capture.capture()); + + Command result = capture.getValue(); + DecimalType dtResult = (DecimalType) result; + assertThat(dtResult.doubleValue(), is(24.0)); + } + + @Test + public void testDecimalTypeOnCommandFromItemWithFloor() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, 0, RoundingMode.FLOOR.name()); + + Command cmd = new DecimalType(23.6); + roundProfile.onCommandFromItem(cmd); + + ArgumentCaptor capture = ArgumentCaptor.forClass(Command.class); + verify(callback, times(1)).handleCommand(capture.capture()); + + Command result = capture.getValue(); + DecimalType dtResult = (DecimalType) result; + assertThat(dtResult.doubleValue(), is(23.0)); + } + + @Test + public void testQuantityTypeOnCommandFromItem() { + ProfileCallback callback = mock(ProfileCallback.class); + RoundStateProfile roundProfile = createProfile(callback, 1); + + Command cmd = new QuantityType("23.333 °C"); + roundProfile.onCommandFromItem(cmd); + + ArgumentCaptor capture = ArgumentCaptor.forClass(Command.class); + verify(callback, times(1)).handleCommand(capture.capture()); + + Command result = capture.getValue(); + @SuppressWarnings("unchecked") + QuantityType qtResult = (QuantityType) result; + assertThat(qtResult.doubleValue(), is(23.3)); + assertThat(qtResult.getUnit(), is(SIUnits.CELSIUS)); + } + + private RoundStateProfile createProfile(ProfileCallback callback, Integer scale) { + return createProfile(callback, scale, null); + } + + private RoundStateProfile createProfile(ProfileCallback callback, Integer scale, @Nullable String mode) { + ProfileContext context = mock(ProfileContext.class); + Configuration config = new Configuration(); + config.put(RoundStateProfile.PARAM_SCALE, scale); + config.put(RoundStateProfile.PARAM_MODE, mode == null ? RoundingMode.HALF_UP.name() : mode); + when(context.getConfiguration()).thenReturn(config); + + return new RoundStateProfile(callback, context); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java new file mode 100644 index 0000000000000..bb16fa7c0f865 --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2010-2024 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.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)); + } + + @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); + 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 + 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() { + 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'")); + } +} diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfileTest.java new file mode 100644 index 0000000000000..75c671586b4fc --- /dev/null +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/ThresholdStateProfileTest.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +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.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +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; + +/** + * Basic unit tests for {@link ThresholdStateProfile}. + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class ThresholdStateProfileTest { + + public static class ParameterSet { + public State state; + public State resultingState; + public Command command; + public Command resultingCommand; + public int treshold; + + public ParameterSet(Type source, Type result, int treshold) { + this.state = (State) source; + this.resultingState = (State) result; + this.command = (Command) source; + this.resultingCommand = (Command) result; + this.treshold = treshold; + } + } + + public static Collection parameters() { + return List.of(new Object[][] { // + { new ParameterSet(PercentType.HUNDRED, OnOffType.OFF, 10) }, // + { new ParameterSet(new PercentType(BigDecimal.valueOf(25)), OnOffType.OFF, 10) }, // + { new ParameterSet(PercentType.ZERO, OnOffType.ON, 10) }, // + { new ParameterSet(PercentType.HUNDRED, OnOffType.OFF, 40) }, // + { new ParameterSet(new PercentType(BigDecimal.valueOf(25)), OnOffType.ON, 40) }, // + { new ParameterSet(PercentType.ZERO, OnOffType.ON, 40) } // + }); + } + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + private @Mock @NonNullByDefault({}) ProfileContext mockContext; + + @ParameterizedTest + @MethodSource("parameters") + public void testOnCommandFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(parameterSet.treshold); + verifySendCommand(profile, parameterSet.command, parameterSet.resultingCommand); + } + + @ParameterizedTest + @MethodSource("parameters") + public void testOnStateUpdateFromHandler(ParameterSet parameterSet) { + final StateProfile profile = initProfile(parameterSet.treshold); + verifySendUpdate(profile, parameterSet.state, parameterSet.resultingState); + } + + private StateProfile initProfile(int threshold) { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("threshold", threshold))); + return new ThresholdStateProfile(mockCallback, mockContext); + } + + private void verifySendCommand(StateProfile profile, Command command, Command result) { + reset(mockCallback); + profile.onCommandFromHandler(command); + verify(mockCallback, times(1)).sendCommand(eq(result)); + } + + private void verifySendUpdate(StateProfile profile, State state, State result) { + reset(mockCallback); + profile.onStateUpdateFromHandler(state); + verify(mockCallback, times(1)).sendUpdate(eq(result)); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index eaaef4bb7a04a..3498b8785089f 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -32,6 +32,7 @@ org.openhab.io.neeo org.openhab.io.openhabcloud + org.openhab.transform.basicprofiles org.openhab.transform.bin2json org.openhab.transform.exec org.openhab.transform.jinja From 2d730d110418217078a2506fc384a82baf81236c Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Sun, 12 May 2024 18:43:14 +0200 Subject: [PATCH 2/4] bom Signed-off-by: Jan N. Klug --- bom/openhab-addons/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e2649c4cda568..1afc73ab02f4a 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -2086,6 +2086,11 @@ org.openhab.persistence.rrd4j ${project.version} + + org.openhab.addons.bundles + org.openhab.transform.basicprofiles + ${project.version} + org.openhab.addons.bundles org.openhab.transform.bin2json From 382470606783d8475452f86d7944eff5f0c3ece1 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Wed, 22 May 2024 18:05:33 +0200 Subject: [PATCH 3/4] address review comments Signed-off-by: Jan N. Klug --- .../README.md | 4 +- .../factory/BasicProfilesFactory.java | 12 +++-- .../GenericCommandTriggerProfile.java | 3 +- .../internal/profiles/InvertStateProfile.java | 9 ++-- .../internal/profiles/RoundStateProfile.java | 8 ++- .../profiles/TimeRangeCommandProfile.java | 11 +++-- .../OH-INF/i18n/basicprofiles_de.properties | 49 ------------------- .../factory/BasicProfilesFactoryTest.java | 5 +- 8 files changed, 28 insertions(+), 73 deletions(-) delete mode 100644 bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties diff --git a/bundles/org.openhab.transform.basicprofiles/README.md b/bundles/org.openhab.transform.basicprofiles/README.md index 7b5e4ee07458c..e934a1fefb7cc 100644 --- a/bundles/org.openhab.transform.basicprofiles/README.md +++ b/bundles/org.openhab.transform.basicprofiles/README.md @@ -62,7 +62,7 @@ Switch debouncedSwitch { channel="xxx" [profile="basic-profiles:debounce-countin ## Debounce (Time) Profile In `LAST` mode this profile delays commands or state updates for a configured number of milliseconds and only send the value if no other value is received with that timespan. -In `FIRST` mode this profile discards values for the configured time after a value is send. +In `FIRST` mode this profile discards values for the configured time after a value is sent. It can be used to debounce Item States/Commands or prevent excessive load on networks. @@ -171,7 +171,7 @@ Possible values for parameter `restoreValue`: ### Full Example -```Java +```java Switch motionSensorFirstFloor { 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"] diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java index 2e7c53f9388fb..8f1da9adedbc6 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactory.java @@ -19,11 +19,11 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.i18n.LocalizedKey; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.thing.Channel; @@ -121,13 +121,16 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService; private final Bundle bundle; private final ItemRegistry itemRegistry; + private final TimeZoneProvider timeZoneProvider; @Activate public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService, - final @Reference BundleResolver bundleResolver, @Reference ItemRegistry itemRegistry) { + final @Reference BundleResolver bundleResolver, @Reference ItemRegistry itemRegistry, + @Reference TimeZoneProvider timeZoneProvider) { this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService; this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class); this.itemRegistry = itemRegistry; + this.timeZoneProvider = timeZoneProvider; } @Override @@ -148,7 +151,7 @@ public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService } else if (THRESHOLD_UID.equals(profileTypeUID)) { return new ThresholdStateProfile(callback, context); } else if (TIME_RANGE_COMMAND_UID.equals(profileTypeUID)) { - return new TimeRangeCommandProfile(callback, context); + return new TimeRangeCommandProfile(callback, context, timeZoneProvider); } else if (STATE_FILTER_UID.equals(profileTypeUID)) { return new StateFilterProfile(callback, context, itemRegistry); } @@ -157,8 +160,7 @@ public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService @Override public Collection getProfileTypes(@Nullable Locale locale) { - return SUPPORTED_PROFILE_TYPES.stream().map(p -> createLocalizedProfileType(p, locale)) - .collect(Collectors.toUnmodifiableList()); + return SUPPORTED_PROFILE_TYPES.stream().map(p -> createLocalizedProfileType(p, locale)).toList(); } @Override diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java index 6ab97c243c798..ef8615f4f548d 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/GenericCommandTriggerProfile.java @@ -59,8 +59,7 @@ public GenericCommandTriggerProfile(ProfileCallback callback, ProfileContext con Object paramValue = context.getConfiguration().get(PARAM_COMMAND); logger.trace("Configuring profile '{}' with '{}' parameter: '{}'", getProfileTypeUID(), PARAM_COMMAND, paramValue); - if (paramValue instanceof String) { - String value = (String) paramValue; + if (paramValue instanceof String value) { command = TypeParser.parseCommand(SUPPORTED_COMMANDS, value); if (command == null) { logger.debug("Value '{}' for parameter '{}' is a not supported command. Using StringType instead.", diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java index 964551aaf1081..579190c17a824 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/InvertStateProfile.java @@ -83,14 +83,11 @@ private Type invert(Type type) { return type; } - if (type instanceof QuantityType) { - QuantityType qtState = (QuantityType) type; + if (type instanceof QuantityType qtState) { return qtState.negate(); - } else if (type instanceof PercentType) { - PercentType ptState = (PercentType) type; + } else if (type instanceof PercentType ptState) { return new PercentType(100 - ptState.intValue()); - } else if (type instanceof DecimalType) { - DecimalType dtState = (DecimalType) type; + } else if (type instanceof DecimalType dtState) { return new DecimalType(-1 * dtState.doubleValue()); } else if (type instanceof IncreaseDecreaseType) { return IncreaseDecreaseType.INCREASE.equals(type) ? IncreaseDecreaseType.DECREASE diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java index 2ed074ef39045..e89d362368371 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/RoundStateProfile.java @@ -57,7 +57,7 @@ public RoundStateProfile(ProfileCallback callback, ProfileContext context) { int localScale = 0; Integer configScale = config.scale; - if (configScale instanceof Number) { + if (configScale != null) { localScale = ((Number) configScale).intValue(); } else { logger.error("Parameter 'scale' is not of type String or Number."); @@ -110,11 +110,9 @@ private Type applyRound(Type state) { } Type result = UnDefType.UNDEF; - if (state instanceof QuantityType) { - QuantityType qtState = (QuantityType) state; + if (state instanceof QuantityType qtState) { result = new QuantityType<>(qtState.toBigDecimal().setScale(scale, roundingMode), qtState.getUnit()); - } else if (state instanceof DecimalType) { - DecimalType dtState = (DecimalType) state; + } else if (state instanceof DecimalType dtState) { result = new DecimalType(dtState.toBigDecimal().setScale(scale, roundingMode)); } else { logger.warn( diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java index e25fd3bac174c..f4dc35d9e0ffc 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/TimeRangeCommandProfile.java @@ -15,7 +15,7 @@ import static org.openhab.transform.basicprofiles.internal.factory.BasicProfilesFactory.TIME_RANGE_COMMAND_UID; import java.time.Duration; -import java.time.ZoneId; +import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.regex.Pattern; @@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.thing.profiles.ProfileCallback; @@ -58,6 +59,7 @@ public class TimeRangeCommandProfile implements StateProfile { private final Logger logger = LoggerFactory.getLogger(TimeRangeCommandProfile.class); private final ProfileCallback callback; + private final TimeZoneProvider timeZoneProvider; private final PercentType inRangeValue; private final PercentType outOfRangeValue; @@ -68,8 +70,11 @@ public class TimeRangeCommandProfile implements StateProfile { private @Nullable PercentType previousState; private @Nullable PercentType restoreState; - public TimeRangeCommandProfile(ProfileCallback callback, ProfileContext context) { + public TimeRangeCommandProfile(ProfileCallback callback, ProfileContext context, + TimeZoneProvider timeZoneProvider) { this.callback = callback; + this.timeZoneProvider = timeZoneProvider; + TimeRangeCommandProfileConfig config = context.getConfiguration().as(TimeRangeCommandProfileConfig.class); logger.debug( "Configuring profile with parameters: [{inRangeValue='{}', outOfRangeValue='{}', start='{}', end='{}', restoreValue='{}']", @@ -194,7 +199,7 @@ public void onCommandFromHandler(Command command) { } private PercentType getOnValue() { - ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()); + ZonedDateTime now = Instant.now().atZone(timeZoneProvider.getTimeZone()); ZonedDateTime today = now.truncatedTo(ChronoUnit.DAYS); return now.isAfter(today.plusMinutes(startTimeInMinutes)) && now.isBefore(today.plusMinutes(endTimeInMinutes)) ? inRangeValue diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties deleted file mode 100644 index e31d8339b0d58..0000000000000 --- a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/i18n/basicprofiles_de.properties +++ /dev/null @@ -1,49 +0,0 @@ -profile-type.basic-profiles.generic-command.label = Generic Command -profile.config.basic-profiles.generic-command.events.label = Ereignisse -profile.config.basic-profiles.generic-command.events.description = Kommagetrennte Liste der Ereignisse, die das Profil verfolgen soll. -profile.config.basic-profiles.generic-command.command.label = Kommando -profile.config.basic-profiles.generic-command.command.description = Kommando, welches gesendet wird, sobald eines der konfigurierten Ereignisse eintritt. - -profile-type.basic-profiles.toggle-switch.label = Generic Toggle Switch -profile.config.basic-profiles.toggle-switch.events.label = Ereignisse -profile.config.basic-profiles.toggle-switch.events.description = Kommagetrennte Liste der Ereignisse, die das Profil verfolgen soll. - -profile-type.basic-profiles.debounce-counting.label = Entprellen (Zähler) -profile.config.basic-profiles.debounce-counting.numberOfChanges.label = Anzahl der Änderungen -profile.config.basic-profiles.debounce-counting.numberOfChanges.description = Anzahl der Änderungen bevor der Item State aktualisiert wird. - -profile-type.basic-profiles.debounce-time.label = Entprellen (Zeit) -profile.config.basic-profiles.debounce-time.toItemDelay.label = Verzögerung Zum Item -profile.config.basic-profiles.debounce-time.toItemDelay.description = Millisekunden bevor das Item Update gesendet wird. -profile.config.basic-profiles.debounce-time.toHandlerDelay.label = Verzögerung Zum Handler -profile.config.basic-profiles.debounce-time.toHandlerDelay.description = Millisekunden bevor das Update an den Handler gesendet wird. -profile.config.basic-profiles.debounce-time.mode.label = Modus -profile.config.basic-profiles.debounce-time.mode.option.FIRST = Sende ersten Wert -profile.config.basic-profiles.debounce-time.mode.option.LAST = Sende letzten Wert - -profile-type.basic-profiles.invert.label = Invertieren / Negieren - -profile-type.basic-profiles.round.label = Runden -profile.config.basic-profiles.round.scale.label = Skalierung -profile.config.basic-profiles.round.scale.description = Skalierung, die auf den Wert angewendet werden soll. -profile.config.basic-profiles.round.mode.label = Rundungsmodus -profile.config.basic-profiles.round.mode.description = Rundungsmodus, der verwendet werden soll (UP, DOWN, CEILING, FLOOR, HALF_UP oder HALF_DOWN). - -profile-type.basic-profiles.threshold.label = Grenzwert -profile.config.basic-profiles.threshold.threshold.label = Grenzwert -profile.config.basic-profiles.threshold.threshold.description = Bildet bei Unterschreitung des Grenzwertes (Vorgabe: 10) ON auf ein Switch Item ab, OFF sonst. - -profile-type.basic-profiles.time-range-command.label = Wert In Zeitspanne -profile.config.basic-profiles.time-range-command.inRangeValue.label = Wert Innerhalb Der Zeitspanne -profile.config.basic-profiles.time-range-command.inRangeValue.description = Der Wert, der gesendet wird, wenn das Profil ON erkennt und die aktuelle Zeit zwischen der Startzeit und der Endzeit liegt. -profile.config.basic-profiles.time-range-command.outOfRangeValue.label = Wert Außerhalb Der Zeitspanne -profile.config.basic-profiles.time-range-command.outOfRangeValue.description = Der Wert, der gesendet wird, wenn das Profil ON erkennt und die aktuelle Zeit NICHT zwischen der Startzeit und der Endzeit liegt. -profile.config.basic-profiles.time-range-command.start.label = Startzeit -profile.config.basic-profiles.time-range-command.start.description = Die Startzeit am Tag (hh:mm). -profile.config.basic-profiles.time-range-command.end.label = Endzeit -profile.config.basic-profiles.time-range-command.end.description = Die Endzeit am Tag (hh:mm). -profile.config.basic-profiles.time-range-command.restoreValue.label = Wert Wiederherstellen -profile.config.basic-profiles.time-range-command.restoreValue.description = Auswählen, was passieren soll, wenn das Profil wieder OFF erkennt. -profile.config.basic-profiles.time-range-command.restoreValue.option.OFF = Aus -profile.config.basic-profiles.time-range-command.restoreValue.option.PREVIOUS = Zurück zum vorherigen Status -profile.config.basic-profiles.time-range-command.restoreValue.option.NOTHING = Unternehme nichts diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java index 0b1127f785def..cb4a71f010282 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java @@ -30,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.i18n.TimeZoneProvider; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.profiles.ProfileCallback; @@ -62,6 +63,7 @@ public class BasicProfilesFactoryTest { private static final Configuration CONFIG = new Configuration(PROPERTIES); private @Mock @NonNullByDefault({}) ProfileTypeI18nLocalizationService mockLocalizationService; + private @Mock @NonNullByDefault({}) TimeZoneProvider mockTimeZoneProvider; private @Mock @NonNullByDefault({}) BundleResolver mockBundleResolver; private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; private @Mock @NonNullByDefault({}) ProfileContext mockContext; @@ -71,7 +73,8 @@ public class BasicProfilesFactoryTest { @BeforeEach public void setup() { - profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver, mockItemRegistry); + profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver, mockItemRegistry, + mockTimeZoneProvider); when(mockContext.getConfiguration()).thenReturn(CONFIG); } From c433bd319862739b72570f67befdc91c4dec7366 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sat, 25 May 2024 08:28:28 +0200 Subject: [PATCH 4/4] Update bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml Co-authored-by: Jacob Laursen Signed-off-by: J-N-K --- .../src/main/resources/OH-INF/addon/addon.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml index a1754d3f64070..a2caab921c075 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.transform.basicprofiles/src/main/resources/OH-INF/addon/addon.xml @@ -6,5 +6,6 @@ transformation Basic Profiles A set of profiles with basic functionality. + local