[basicprofiles] Initial contribution (#16754)

* [basicprofiles] Initial contribution

A set of basic profiles with general use cases. See documentation for details.

Also-By: Christoph Weitkamp <github@christophweitkamp.de>
Also-By: Arne Seime <arne.seime@gmail.com>
Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2024-05-28 23:30:46 +02:00 committed by GitHub
parent dcd778301b
commit 706f9ca6b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 3240 additions and 0 deletions

View File

@ -2111,6 +2111,11 @@
<artifactId>org.openhab.persistence.rrd4j</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.transform.basicprofiles</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.transform.bin2json</artifactId>

View File

@ -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
*/

View File

@ -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 sent.
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"]
}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.transform.basicprofiles</artifactId>
<name>openHAB Add-ons :: Bundles :: Transformation Service :: Basic Profiles</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.transform.basicprofiles-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-transformation-basicprofiles" description="Basic Profiles" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="75">mvn:org.openhab.addons.bundles/org.openhab.transform.basicprofiles/${project.version}</bundle>
</feature>
</features>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ",";
}

View File

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

View File

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

View File

@ -0,0 +1,199 @@
/**
* 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 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;
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<ProfileTypeUID> 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<ProfileType> 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<LocalizedKey, ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();
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,
@Reference TimeZoneProvider timeZoneProvider) {
this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService;
this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class);
this.itemRegistry = itemRegistry;
this.timeZoneProvider = timeZoneProvider;
}
@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, timeZoneProvider);
} else if (STATE_FILTER_UID.equals(profileTypeUID)) {
return new StateFilterProfile(callback, context, itemRegistry);
}
return null;
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return SUPPORTED_PROFILE_TYPES.stream().map(p -> createLocalizedProfileType(p, locale)).toList();
}
@Override
public Collection<ProfileTypeUID> 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;
}
}
}

View File

@ -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<String> 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<String> values = new ArrayList<>();
for (String event : (Iterable<String>) 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();
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,91 @@
/**
* 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<Class<? extends Command>> 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 value) {
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
}
}

View File

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

View File

@ -0,0 +1,116 @@
/**
* 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<?> qtState) {
return qtState.negate();
} else if (type instanceof PercentType ptState) {
return new PercentType(100 - ptState.intValue());
} else if (type instanceof DecimalType dtState) {
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;
}
}
}

View File

@ -0,0 +1,125 @@
/**
* 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 != null) {
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<?> qtState) {
result = new QuantityType<>(qtState.toBigDecimal().setScale(scale, roundingMode), qtState.getUnit());
} else if (state instanceof DecimalType dtState) {
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;
}
}

View File

@ -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<Class<? extends State>> acceptedDataTypes;
private List<StateCondition> 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<StateCondition> parseConditions(@Nullable String config, String separator) {
if (config == null) {
return List.of();
}
List<StateCondition> 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 <EQ|NEQ> 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
+ "'}'";
}
}
}

View File

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

View File

@ -0,0 +1,222 @@
/**
* 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.Instant;
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.i18n.TimeZoneProvider;
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 TimeZoneProvider timeZoneProvider;
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,
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='{}']",
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 = Instant.now().atZone(timeZoneProvider.getTimeZone());
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;
}
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="basicprofiles" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>transformation</type>
<name>Basic Profiles</name>
<description>A set of profiles with basic functionality.</description>
<connection>local</connection>
</addon:addon>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:debounce-counting">
<parameter name="numberOfChanges" type="integer" min="0" step="1">
<label>Number Of Changes</label>
<description>Number of changes before updating Item State.</description>
<default>1</default>
</parameter>
</config-description>
<config-description uri="profile:basic-profiles:debounce-time">
<parameter name="toItemDelay" type="integer" min="0" step="1" unit="ms">
<label>To Item Delay</label>
<description>Timespan before an value is forwarded to the item (or discarded after the first value).</description>
<default>0</default>
</parameter>
<parameter name="toHandlerDelay" type="integer" min="0" step="1" unit="ms">
<label>To Handler Delay</label>
<description>Timespan before an value is forwarded to the handler (or discarded after the first value).</description>
<default>0</default>
</parameter>
<parameter name="mode" type="text">
<label>Mode</label>
<options>
<option value="FIRST">Send first value</option>
<option value="LAST">Send last value</option>
</options>
<default>LAST</default>
<limitToOptions>true</limitToOptions>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:generic-command">
<parameter name="events" type="text" required="true" multiple="true">
<label>Events</label>
<description>Comma separated list of events to which the profile should listen.</description>
</parameter>
<parameter name="command" type="text" required="true">
<label>Command</label>
<description>Command which should be sent if the event is triggered.</description>
<limitToOptions>false</limitToOptions>
<options>
<option value="INCREASE">INCREASE</option>
<option value="DECREASE">DECREASE</option>
<option value="NEXT">NEXT</option>
<option value="PREVIOUS">PREVIOUS</option>
<option value="ON">ON</option>
<option value="OFF">OFF</option>
<option value="PLAY">PLAY</option>
<option value="PAUSE">PAUSE</option>
<option value="REWIND">REWIND</option>
<option value="FASTFORWARD">FASTFORWARD</option>
<option value="STOP">STOP</option>
<option value="MOVE">MOVE</option>
<option value="UP">UP</option>
<option value="DOWN">DOWN</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:round">
<parameter name="scale" type="integer" min="-16" max="16" step="1" required="true">
<label>Scale</label>
<description>Scale to indicate the resulting number of decimal places.</description>
</parameter>
<parameter name="mode" type="text">
<label>Rounding Mode</label>
<description>Rounding mode to be used (e.g. "UP", "DOWN", "CEILING", "FLOOR", "HALF_UP" or "HALF_DOWN").
</description>
<default>HALF_UP</default>
<options>
<option value="UP">UP</option>
<option value="DOWN">DOWN</option>
<option value="CEILING">CEILING</option>
<option value="FLOOR">FLOOR</option>
<option value="HALF_UP">HALF_UP</option>
<option value="HALF_DOWN">HALF_DOWN</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:state-filter">
<parameter name="conditions" type="text" required="true">
<label>Conditions</label>
<description>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'".</description>
</parameter>
<parameter name="mismatchState" type="text">
<label>State for filter rejects</label>
<description>State to pass to item instead if conditions are NOT met. Use quotes to treat as `StringType`</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:threshold">
<parameter name="threshold" type="integer" min="0" max="100" step="1">
<label>Threshold</label>
<description>Triggers ON if value is below the given threshold, otherwise OFF.</description>
<default>10</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:time-range-command">
<parameter name="inRangeValue" type="integer" min="0" max="100" step="1">
<label>In Range Value</label>
<description>The value which will be send when the profile detects ON and current time is between start time and end
time.</description>
<default>100</default>
</parameter>
<parameter name="outOfRangeValue" type="integer" min="0" max="100" step="1">
<label>Out Of Range Value</label>
<description>The value which will be send when the profile detects ON and current time is NOT between start time and
end time.</description>
<default>30</default>
</parameter>
<parameter name="start" type="text" pattern="^([0-1][0-9]|2[0-3])(:[0-5][0-9])$" required="true">
<label>Start Time</label>
<description>The start time of the day (hh:mm).</description>
<context>time</context>
</parameter>
<parameter name="end" type="text" pattern="^([0-1][0-9]|2[0-3])(:[0-5][0-9])$" required="true">
<label>End Time</label>
<description>The end time of the day (hh:mm).</description>
<context>time</context>
</parameter>
<parameter name="restoreValue" type="text">
<advanced>true</advanced>
<label>Restore Value</label>
<description>Select what should happen when the profile detects OFF again.</description>
<options>
<option value="OFF">Off</option>
<option value="PREVIOUS">Return to previous value</option>
<option value="NOTHING">Do nothing</option>
</options>
<limitToOptions>false</limitToOptions>
<default>OFF</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:basic-profiles:toggle-switch">
<parameter name="events" type="text" required="true" multiple="true">
<label>Events</label>
<description>Comma separated list of events to which the profile should listen.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

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

View File

@ -0,0 +1,101 @@
/**
* 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.i18n.TimeZoneProvider;
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<String, Object> 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({}) TimeZoneProvider mockTimeZoneProvider;
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,
mockTimeZoneProvider);
when(mockContext.getConfiguration()).thenReturn(CONFIG);
}
@Test
public void systemProfileTypesAndUidsShouldBeAvailable() {
Collection<ProfileTypeUID> supportedProfileTypeUIDs = profileFactory.getSupportedProfileTypeUIDs();
assertThat(supportedProfileTypeUIDs, hasSize(NUMBER_OF_PROFILES));
Collection<ProfileType> 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));
}
}
}

View File

@ -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<State> sourceStates;
public final List<State> resultingStates;
public final int numberOfChanges;
public ParameterSet(List<State> sourceStates, List<State> resultingStates, int numberOfChanges) {
this.sourceStates = sourceStates;
this.resultingStates = resultingStates;
this.numberOfChanges = numberOfChanges;
}
}
public static Collection<Object[]> 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));
}
}

View File

@ -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<Object[]> 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));
}
}

View File

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

View File

@ -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<Object[]> 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));
}
}

View File

@ -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<Temperature> 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<Command> 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<Command> 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<Command> 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<Command> 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<Temperature>("23.333 °C");
roundProfile.onCommandFromItem(cmd);
ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
verify(callback, times(1)).handleCommand(capture.capture());
Command result = capture.getValue();
@SuppressWarnings("unchecked")
QuantityType<Temperature> qtResult = (QuantityType<Temperature>) 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);
}
}

View File

@ -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'"));
}
}

View File

@ -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<Object[]> 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));
}
}

View File

@ -32,6 +32,7 @@
<module>org.openhab.io.neeo</module>
<module>org.openhab.io.openhabcloud</module>
<!-- transformations -->
<module>org.openhab.transform.basicprofiles</module>
<module>org.openhab.transform.bin2json</module>
<module>org.openhab.transform.exec</module>
<module>org.openhab.transform.jinja</module>