mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[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:
parent
dcd778301b
commit
706f9ca6b5
@ -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>
|
||||
|
32
bundles/org.openhab.transform.basicprofiles/NOTICE
Normal file
32
bundles/org.openhab.transform.basicprofiles/NOTICE
Normal 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
|
||||
*/
|
210
bundles/org.openhab.transform.basicprofiles/README.md
Normal file
210
bundles/org.openhab.transform.basicprofiles/README.md
Normal 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"]
|
||||
}
|
||||
```
|
17
bundles/org.openhab.transform.basicprofiles/pom.xml
Normal file
17
bundles/org.openhab.transform.basicprofiles/pom.xml
Normal 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>
|
@ -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>
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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 = ",";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
+ "'}'";
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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'"));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user