mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[basicprofiles] Add State Delay profile (#20929)
* [basicprofiles] Add State Delay profile Signed-off-by: Andreas Berger <andreas@berger-freelancer.com>
This commit is contained in:
@@ -3,11 +3,12 @@
|
|||||||
This bundle provides a list of useful profiles:
|
This bundle provides a list of useful profiles:
|
||||||
|
|
||||||
| Profile | Description |
|
| Profile | Description |
|
||||||
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||||
| [Generic Command Profile](#generic-command-profile) | Sends a command to the Item when an event is triggered |
|
| [Generic Command Profile](#generic-command-profile) | Sends a command to the Item when an event is triggered |
|
||||||
| [Generic Toggle Switch Profile](#generic-toggle-switch-profile) | Toggles a Switch Item when an event is triggered |
|
| [Generic Toggle Switch Profile](#generic-toggle-switch-profile) | Toggles a Switch Item when an event is triggered |
|
||||||
| [Debounce (Counting) Profile](#debounce-counting-profile) | Counts and skips a number of state changes |
|
| [Debounce (Counting) Profile](#debounce-counting-profile) | Counts and skips a number of state changes |
|
||||||
| [Debounce (Time) Profile](#debounce-time-profile) | Reduces the frequency of commands or state updates |
|
| [Debounce (Time) Profile](#debounce-time-profile) | Reduces the frequency of commands or state updates |
|
||||||
|
| [Debounce (State) Profile](#debounce-state-profile) | Delays ON/OFF (or OPEN/CLOSED) state changes per direction with an independent cooldown |
|
||||||
| [Invert / Negate Profile](#invert-negate-profile) | Inverts or negates a command or state |
|
| [Invert / Negate Profile](#invert-negate-profile) | Inverts or negates a command or state |
|
||||||
| [Round Profile](#round-profile) | Rounds numeric and DateTime input data |
|
| [Round Profile](#round-profile) | Rounds numeric and DateTime input data |
|
||||||
| [Threshold Profile](#threshold-profile) | Translates numeric input data to `ON` or `OFF` based on a threshold value |
|
| [Threshold Profile](#threshold-profile) | Translates numeric input data to `ON` or `OFF` based on a threshold value |
|
||||||
@@ -94,6 +95,36 @@ It can be used to debounce Item states/commands or prevent excessive load on net
|
|||||||
Number:Temperature debouncedSetpoint { channel="xxx" [profile="basic-profiles:debounce-time", toHandlerDelay=1000] }
|
Number:Temperature debouncedSetpoint { channel="xxx" [profile="basic-profiles:debounce-time", toHandlerDelay=1000] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Debounce (State) Profile
|
||||||
|
|
||||||
|
Delays `ON`/`OFF` (Switch) or `OPEN`/`CLOSED` (Contact) state updates coming from the handler, with an independent delay per direction.
|
||||||
|
An `ON`/`OPEN` value is held for `onDelay` milliseconds, an `OFF`/`CLOSED` value for `offDelay` milliseconds.
|
||||||
|
A pending value is cancelled if the opposite value arrives before the delay elapses, and repeated identical values do not restart the timer.
|
||||||
|
|
||||||
|
A typical use is a "cooldown" for a flapping binary sensor: forward `ON` immediately (`onDelay=0`) but keep it `ON` for a while after the device reports `OFF` (`offDelay` set to the cooldown), so it does not rapidly switch on and off.
|
||||||
|
|
||||||
|
### Debounce (Time) vs. Debounce (State)
|
||||||
|
|
||||||
|
Both profiles smooth out noisy inputs, but they target different cases:
|
||||||
|
|
||||||
|
- Use **Debounce (Time)** for a single, type-agnostic delay that applies to every value regardless of its content (numbers, strings, switches, …). It is the right choice when you simply want to throttle the update/command frequency of a channel.
|
||||||
|
- Use **Debounce (State)** for binary `Switch`/`Contact` channels when you need a _different_ delay depending on the direction of the change. Only the active→inactive transition (or vice versa) is delayed, the opposite transition can be forwarded immediately, and an opposing value cancels a still-pending one. This makes it ideal for an asymmetric "cooldown" (e.g. react to `ON` at once but linger on `OFF`) that `debounce-time` cannot express.
|
||||||
|
|
||||||
|
### Debounce (State) Profile Configuration
|
||||||
|
|
||||||
|
| Configuration Parameter | Type | Description |
|
||||||
|
|-------------------------|---------|-----------------------------------------------------------------------------------------------|
|
||||||
|
| `onDelay` | integer | Delay in ms before an `ON` (Switch) / `OPEN` (Contact) value is forwarded. `0` = immediate. |
|
||||||
|
| `offDelay` | integer | Delay in ms before an `OFF` (Switch) / `CLOSED` (Contact) value is forwarded. `0` = immediate. |
|
||||||
|
|
||||||
|
### Debounce (State) Profile Example
|
||||||
|
|
||||||
|
Hold the "is it raining" status `ON` for 2 minutes after the device reports dry:
|
||||||
|
|
||||||
|
```java
|
||||||
|
Switch Raining { channel="fineoffsetweatherstation:gateway:xxx:rain-state" [profile="basic-profiles:debounce-state", onDelay=0, offDelay=120000] }
|
||||||
|
```
|
||||||
|
|
||||||
## Invert / Negate Profile<a id="invert-negate-profile"></a>
|
## Invert / Negate Profile<a id="invert-negate-profile"></a>
|
||||||
|
|
||||||
The Invert / Negate Profile inverts or negates a command or state.
|
The Invert / Negate Profile inverts or negates a command or state.
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.DebounceStateProfile}.
|
||||||
|
*
|
||||||
|
* @author Andreas Berger - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class DebounceStateProfileConfig {
|
||||||
|
public int onDelay = 0;
|
||||||
|
public int offDelay = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DebounceStateProfileConfig{onDelay=" + onDelay + ", offDelay=" + offDelay + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-4
@@ -42,6 +42,7 @@ import org.openhab.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService;
|
|||||||
import org.openhab.core.thing.type.ChannelType;
|
import org.openhab.core.thing.type.ChannelType;
|
||||||
import org.openhab.core.util.BundleResolver;
|
import org.openhab.core.util.BundleResolver;
|
||||||
import org.openhab.transform.basicprofiles.internal.profiles.DebounceCountingStateProfile;
|
import org.openhab.transform.basicprofiles.internal.profiles.DebounceCountingStateProfile;
|
||||||
|
import org.openhab.transform.basicprofiles.internal.profiles.DebounceStateProfile;
|
||||||
import org.openhab.transform.basicprofiles.internal.profiles.DebounceTimeStateProfile;
|
import org.openhab.transform.basicprofiles.internal.profiles.DebounceTimeStateProfile;
|
||||||
import org.openhab.transform.basicprofiles.internal.profiles.GenericCommandTriggerProfile;
|
import org.openhab.transform.basicprofiles.internal.profiles.GenericCommandTriggerProfile;
|
||||||
import org.openhab.transform.basicprofiles.internal.profiles.GenericToggleSwitchTriggerProfile;
|
import org.openhab.transform.basicprofiles.internal.profiles.GenericToggleSwitchTriggerProfile;
|
||||||
@@ -70,6 +71,7 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
|
|||||||
public static final ProfileTypeUID GENERIC_TOGGLE_SWITCH_UID = new ProfileTypeUID(SCOPE, "toggle-switch");
|
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_COUNTING_UID = new ProfileTypeUID(SCOPE, "debounce-counting");
|
||||||
public static final ProfileTypeUID DEBOUNCE_TIME_UID = new ProfileTypeUID(SCOPE, "debounce-time");
|
public static final ProfileTypeUID DEBOUNCE_TIME_UID = new ProfileTypeUID(SCOPE, "debounce-time");
|
||||||
|
public static final ProfileTypeUID DEBOUNCE_STATE_UID = new ProfileTypeUID(SCOPE, "debounce-state");
|
||||||
public static final ProfileTypeUID INVERT_UID = new ProfileTypeUID(SCOPE, "invert");
|
public static final ProfileTypeUID INVERT_UID = new ProfileTypeUID(SCOPE, "invert");
|
||||||
public static final ProfileTypeUID ROUND_UID = new ProfileTypeUID(SCOPE, "round");
|
public static final ProfileTypeUID ROUND_UID = new ProfileTypeUID(SCOPE, "round");
|
||||||
public static final ProfileTypeUID THRESHOLD_UID = new ProfileTypeUID(SCOPE, "threshold");
|
public static final ProfileTypeUID THRESHOLD_UID = new ProfileTypeUID(SCOPE, "threshold");
|
||||||
@@ -91,6 +93,11 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
|
|||||||
.newState(DEBOUNCE_COUNTING_UID, "Debounce (Counting)").build();
|
.newState(DEBOUNCE_COUNTING_UID, "Debounce (Counting)").build();
|
||||||
private static final ProfileType PROFILE_TYPE_DEBOUNCE_TIME = ProfileTypeBuilder
|
private static final ProfileType PROFILE_TYPE_DEBOUNCE_TIME = ProfileTypeBuilder
|
||||||
.newState(DEBOUNCE_TIME_UID, "Debounce (Time)").build();
|
.newState(DEBOUNCE_TIME_UID, "Debounce (Time)").build();
|
||||||
|
private static final ProfileType PROFILE_TYPE_DEBOUNCE_STATE = ProfileTypeBuilder
|
||||||
|
.newState(DEBOUNCE_STATE_UID, "Debounce (State)") //
|
||||||
|
.withSupportedItemTypes(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT) //
|
||||||
|
.withSupportedItemTypesOfChannel(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT) //
|
||||||
|
.build();
|
||||||
private static final ProfileType PROFILE_TYPE_INVERT = ProfileTypeBuilder.newState(INVERT_UID, "Invert / Negate")
|
private static final ProfileType PROFILE_TYPE_INVERT = ProfileTypeBuilder.newState(INVERT_UID, "Invert / Negate")
|
||||||
.withSupportedItemTypes(CoreItemFactory.CONTACT, CoreItemFactory.DIMMER, CoreItemFactory.NUMBER,
|
.withSupportedItemTypes(CoreItemFactory.CONTACT, CoreItemFactory.DIMMER, CoreItemFactory.NUMBER,
|
||||||
CoreItemFactory.PLAYER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH) //
|
CoreItemFactory.PLAYER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH) //
|
||||||
@@ -120,12 +127,14 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GENERIC_COMMAND_UID,
|
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,
|
GENERIC_TOGGLE_SWITCH_UID, DEBOUNCE_COUNTING_UID, DEBOUNCE_TIME_UID, DEBOUNCE_STATE_UID, INVERT_UID,
|
||||||
TIME_RANGE_COMMAND_UID, STATE_FILTER_UID, INACTIVITY_UID, TIME_WEIGHTED_AVERAGE_UID);
|
ROUND_UID, THRESHOLD_UID, TIME_RANGE_COMMAND_UID, STATE_FILTER_UID, INACTIVITY_UID,
|
||||||
|
TIME_WEIGHTED_AVERAGE_UID);
|
||||||
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(PROFILE_TYPE_GENERIC_COMMAND,
|
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_GENERIC_TOGGLE_SWITCH, PROFILE_TYPE_DEBOUNCE_COUNTING, PROFILE_TYPE_DEBOUNCE_TIME,
|
||||||
PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD, PROFILE_TYPE_TIME_RANGE_COMMAND,
|
PROFILE_TYPE_DEBOUNCE_STATE, PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD,
|
||||||
PROFILE_STATE_FILTER, PROFILE_TYPE_INACTIVITY, PROFILE_TIME_WEIGHTED_AVERAGE);
|
PROFILE_TYPE_TIME_RANGE_COMMAND, PROFILE_STATE_FILTER, PROFILE_TYPE_INACTIVITY,
|
||||||
|
PROFILE_TIME_WEIGHTED_AVERAGE);
|
||||||
|
|
||||||
private final Map<LocalizedKey, ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();
|
private final Map<LocalizedKey, ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -158,6 +167,8 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
|
|||||||
return new DebounceCountingStateProfile(callback, context);
|
return new DebounceCountingStateProfile(callback, context);
|
||||||
} else if (DEBOUNCE_TIME_UID.equals(profileTypeUID)) {
|
} else if (DEBOUNCE_TIME_UID.equals(profileTypeUID)) {
|
||||||
return new DebounceTimeStateProfile(callback, context);
|
return new DebounceTimeStateProfile(callback, context);
|
||||||
|
} else if (DEBOUNCE_STATE_UID.equals(profileTypeUID)) {
|
||||||
|
return new DebounceStateProfile(callback, context);
|
||||||
} else if (INVERT_UID.equals(profileTypeUID)) {
|
} else if (INVERT_UID.equals(profileTypeUID)) {
|
||||||
return new InvertStateProfile(callback);
|
return new InvertStateProfile(callback);
|
||||||
} else if (ROUND_UID.equals(profileTypeUID)) {
|
} else if (ROUND_UID.equals(profileTypeUID)) {
|
||||||
|
|||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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_STATE_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.library.types.OnOffType;
|
||||||
|
import org.openhab.core.library.types.OpenClosedType;
|
||||||
|
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.DebounceStateProfileConfig;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delays an {@link OnOffType}/{@link OpenClosedType} {@link State} per direction. Active values (ON/OPEN) are held for
|
||||||
|
* {@code onDelay}, inactive values (OFF/CLOSED) for {@code offDelay}. A pending forward is cancelled by an opposing
|
||||||
|
* value; repeated identical values do not reschedule.
|
||||||
|
*
|
||||||
|
* @author Andreas Berger - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class DebounceStateProfile implements StateProfile {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(DebounceStateProfile.class);
|
||||||
|
|
||||||
|
private final ProfileCallback callback;
|
||||||
|
private final DebounceStateProfileConfig config;
|
||||||
|
private final ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
private @Nullable State lastForwarded;
|
||||||
|
private @Nullable Boolean pendingActive;
|
||||||
|
private @Nullable ScheduledFuture<?> pendingJob;
|
||||||
|
private long pendingGeneration;
|
||||||
|
|
||||||
|
public DebounceStateProfile(ProfileCallback callback, ProfileContext context) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.scheduler = context.getExecutorService();
|
||||||
|
this.config = context.getConfiguration().as(DebounceStateProfileConfig.class);
|
||||||
|
logger.debug("Configuring profile with parameters: {}", config);
|
||||||
|
|
||||||
|
if (config.onDelay < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("onDelay has to be a non-negative integer but was '%d'.", config.onDelay));
|
||||||
|
}
|
||||||
|
if (config.offDelay < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("offDelay has to be a non-negative integer but was '%d'.", config.offDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProfileTypeUID getProfileTypeUID() {
|
||||||
|
return DEBOUNCE_STATE_UID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateUpdateFromItem(State state) {
|
||||||
|
// no-op: this profile only shapes the handler -> item direction
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCommandFromItem(Command command) {
|
||||||
|
callback.handleCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCommandFromHandler(Command command) {
|
||||||
|
// commands are not state and are forwarded immediately; only state updates are delayed
|
||||||
|
callback.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onStateUpdateFromHandler(State state) {
|
||||||
|
Boolean active = asActive(state);
|
||||||
|
if (active == null) {
|
||||||
|
// non-binary values (e.g. UNDEF/NULL) are forwarded immediately and reset the delay state
|
||||||
|
cancelPending();
|
||||||
|
doForward(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. a job for the same value is already pending -> let the running timer continue
|
||||||
|
if (pendingJob != null && active.equals(pendingActive)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. a job for the opposite value is pending -> cancel it
|
||||||
|
if (pendingJob != null) {
|
||||||
|
cancelPending();
|
||||||
|
}
|
||||||
|
// 3. already in the target state -> nothing to forward
|
||||||
|
State last = lastForwarded;
|
||||||
|
if (last != null && active.equals(asActive(last))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 4. forward now or schedule
|
||||||
|
int delay = active ? config.onDelay : config.offDelay;
|
||||||
|
if (delay <= 0) {
|
||||||
|
doForward(state);
|
||||||
|
} else {
|
||||||
|
pendingActive = active;
|
||||||
|
long generation = ++pendingGeneration;
|
||||||
|
pendingJob = scheduler.schedule(() -> {
|
||||||
|
synchronized (DebounceStateProfile.this) {
|
||||||
|
if (generation != pendingGeneration) {
|
||||||
|
// superseded or cancelled while we waited for the lock
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doForward(state);
|
||||||
|
pendingJob = null;
|
||||||
|
pendingActive = null;
|
||||||
|
}
|
||||||
|
}, delay, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doForward(State state) {
|
||||||
|
lastForwarded = state;
|
||||||
|
callback.sendUpdate(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelPending() {
|
||||||
|
ScheduledFuture<?> job = pendingJob;
|
||||||
|
if (job != null) {
|
||||||
|
job.cancel(false);
|
||||||
|
}
|
||||||
|
// invalidate any already-running task that is waiting for the lock
|
||||||
|
pendingGeneration++;
|
||||||
|
pendingJob = null;
|
||||||
|
pendingActive = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Boolean asActive(State state) {
|
||||||
|
if (state instanceof OnOffType onOff) {
|
||||||
|
return onOff == OnOffType.ON;
|
||||||
|
}
|
||||||
|
if (state instanceof OpenClosedType openClosed) {
|
||||||
|
return openClosed == OpenClosedType.OPEN;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?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-state">
|
||||||
|
<parameter name="onDelay" type="integer" min="0" step="1" unit="ms">
|
||||||
|
<label>On Delay</label>
|
||||||
|
<description>Delay before an ON (Switch) / OPEN (Contact) value is forwarded to the Item. 0 forwards immediately.</description>
|
||||||
|
<default>0</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="offDelay" type="integer" min="0" step="1" unit="ms">
|
||||||
|
<label>Off Delay</label>
|
||||||
|
<description>Delay before an OFF (Switch) / CLOSED (Contact) value is forwarded to the Item. 0 forwards immediately.</description>
|
||||||
|
<default>0</default>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</config-description:config-descriptions>
|
||||||
+5
@@ -5,6 +5,10 @@ addon.basicprofiles.description = A set of profiles with basic functionality.
|
|||||||
|
|
||||||
profile.config.basic-profiles.debounce-counting.numberOfChanges.label = Number of Changes
|
profile.config.basic-profiles.debounce-counting.numberOfChanges.label = Number of Changes
|
||||||
profile.config.basic-profiles.debounce-counting.numberOfChanges.description = Number of changes before updating the Item state.
|
profile.config.basic-profiles.debounce-counting.numberOfChanges.description = Number of changes before updating the Item state.
|
||||||
|
profile.config.basic-profiles.debounce-state.onDelay.label = On Delay
|
||||||
|
profile.config.basic-profiles.debounce-state.onDelay.description = Delay before an ON (Switch) / OPEN (Contact) value is forwarded to the Item. 0 forwards immediately.
|
||||||
|
profile.config.basic-profiles.debounce-state.offDelay.label = Off Delay
|
||||||
|
profile.config.basic-profiles.debounce-state.offDelay.description = Delay before an OFF (Switch) / CLOSED (Contact) value is forwarded to the Item. 0 forwards immediately.
|
||||||
profile.config.basic-profiles.debounce-time.mode.label = Mode
|
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.FIRST = Send first value
|
||||||
profile.config.basic-profiles.debounce-time.mode.option.LAST = Send last value
|
profile.config.basic-profiles.debounce-time.mode.option.LAST = Send last value
|
||||||
@@ -77,6 +81,7 @@ profile.config.basic-profiles.toggle-switch.events.description = Comma-separated
|
|||||||
# add-on
|
# add-on
|
||||||
|
|
||||||
profile-type.basic-profiles.debounce-counting.label = Debounce (Counting)
|
profile-type.basic-profiles.debounce-counting.label = Debounce (Counting)
|
||||||
|
profile-type.basic-profiles.debounce-state.label = Debounce (State)
|
||||||
profile-type.basic-profiles.debounce-time.label = Debounce (Time)
|
profile-type.basic-profiles.debounce-time.label = Debounce (Time)
|
||||||
profile-type.basic-profiles.generic-command.label = Generic Command
|
profile-type.basic-profiles.generic-command.label = Generic Command
|
||||||
profile-type.basic-profiles.invert.label = Invert / Negate
|
profile-type.basic-profiles.invert.label = Invert / Negate
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ import org.openhab.transform.basicprofiles.internal.profiles.TimeRangeCommandPro
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class BasicProfilesFactoryTest {
|
public class BasicProfilesFactoryTest {
|
||||||
|
|
||||||
private static final int NUMBER_OF_PROFILES = 11;
|
private static final int NUMBER_OF_PROFILES = 12;
|
||||||
|
|
||||||
private static final Map<String, Object> PROPERTIES = Map.of(ThresholdStateProfile.PARAM_THRESHOLD, 15,
|
private static final Map<String, Object> PROPERTIES = Map.of(ThresholdStateProfile.PARAM_THRESHOLD, 15,
|
||||||
RoundStateProfile.PARAM_SCALE, 2, GenericCommandTriggerProfile.PARAM_EVENTS, "1002,1003",
|
RoundStateProfile.PARAM_SCALE, 2, GenericCommandTriggerProfile.PARAM_EVENTS, "1002,1003",
|
||||||
|
|||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
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.library.types.OpenClosedType;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileContext;
|
||||||
|
import org.openhab.core.thing.profiles.StateProfile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for {@link DebounceStateProfile}.
|
||||||
|
*
|
||||||
|
* @author Andreas Berger - Initial contribution
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@NonNullByDefault
|
||||||
|
class DebounceStateProfileTest {
|
||||||
|
|
||||||
|
private @Mock @NonNullByDefault({}) ProfileCallback mockCallback;
|
||||||
|
private @Mock @NonNullByDefault({}) ProfileContext mockContext;
|
||||||
|
private @Mock @NonNullByDefault({}) ScheduledExecutorService mockScheduler;
|
||||||
|
private @Mock @NonNullByDefault({}) ScheduledFuture<?> mockFuture;
|
||||||
|
|
||||||
|
private StateProfile initProfile(int onDelay, int offDelay) {
|
||||||
|
reset(mockContext, mockCallback, mockScheduler);
|
||||||
|
when(mockContext.getExecutorService()).thenReturn(mockScheduler);
|
||||||
|
when(mockContext.getConfiguration())
|
||||||
|
.thenReturn(new Configuration(Map.of("onDelay", onDelay, "offDelay", offDelay)));
|
||||||
|
lenient().doReturn(mockFuture).when(mockScheduler).schedule(any(Runnable.class), anyLong(),
|
||||||
|
eq(TimeUnit.MILLISECONDS));
|
||||||
|
return new DebounceStateProfile(mockCallback, mockContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable captureScheduled(int expectedDelay) {
|
||||||
|
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||||
|
verify(mockScheduler).schedule(captor.capture(), eq((long) expectedDelay), eq(TimeUnit.MILLISECONDS));
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNegativeOnDelayThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> initProfile(-1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNegativeOffDelayThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> initProfile(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testZeroDelayForwardsImmediately() {
|
||||||
|
StateProfile profile = initProfile(0, 0);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.OFF);
|
||||||
|
verify(mockScheduler, never()).schedule(any(Runnable.class), anyLong(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOffDelayHoldsThenFires() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.ON);
|
||||||
|
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockCallback, never()).sendUpdate(OnOffType.OFF);
|
||||||
|
Runnable job = captureScheduled(5000);
|
||||||
|
|
||||||
|
job.run();
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReturningOnCancelsPendingOff() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockScheduler, times(1)).schedule(any(Runnable.class), eq(5000L), eq(TimeUnit.MILLISECONDS));
|
||||||
|
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
verify(mockFuture, times(1)).cancel(false);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.ON);
|
||||||
|
verify(mockCallback, never()).sendUpdate(OnOffType.OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStaleCancelledJobDoesNotForward() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
Runnable staleOffJob = captureScheduled(5000);
|
||||||
|
|
||||||
|
// ON returns and cancels the pending OFF before its timer effectively completes
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
|
||||||
|
// the stale OFF job runs late (it had already started waiting for the lock)
|
||||||
|
staleOffJob.run();
|
||||||
|
|
||||||
|
// it must NOT forward the stale OFF
|
||||||
|
verify(mockCallback, never()).sendUpdate(OnOffType.OFF);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRepeatedOffDoesNotReschedule() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockScheduler, times(1)).schedule(any(Runnable.class), eq(5000L), eq(TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnDelayHoldsThenFires() {
|
||||||
|
StateProfile profile = initProfile(5000, 0);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
verify(mockCallback, never()).sendUpdate(OnOffType.ON);
|
||||||
|
Runnable job = captureScheduled(5000);
|
||||||
|
job.run();
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnDelayCancelledByOff() {
|
||||||
|
StateProfile profile = initProfile(5000, 0);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
verify(mockScheduler, times(1)).schedule(any(Runnable.class), eq(5000L), eq(TimeUnit.MILLISECONDS));
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockFuture, times(1)).cancel(false);
|
||||||
|
verify(mockCallback, never()).sendUpdate(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContactMapping() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OpenClosedType.OPEN);
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OpenClosedType.OPEN);
|
||||||
|
|
||||||
|
profile.onStateUpdateFromHandler(OpenClosedType.CLOSED);
|
||||||
|
verify(mockCallback, never()).sendUpdate(OpenClosedType.CLOSED);
|
||||||
|
Runnable job = captureScheduled(5000);
|
||||||
|
job.run();
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OpenClosedType.CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCommandsFromHandlerForwardImmediately() {
|
||||||
|
StateProfile profile = initProfile(5000, 5000);
|
||||||
|
profile.onCommandFromHandler(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).sendCommand(OnOffType.ON);
|
||||||
|
profile.onCommandFromHandler(OnOffType.OFF);
|
||||||
|
verify(mockCallback, times(1)).sendCommand(OnOffType.OFF);
|
||||||
|
verify(mockScheduler, never()).schedule(any(Runnable.class), anyLong(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCommandFromHandlerDoesNotAffectPendingStateUpdate() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromHandler(OnOffType.OFF);
|
||||||
|
Runnable job = captureScheduled(5000);
|
||||||
|
|
||||||
|
// a command arriving while an OFF state update is pending must not interfere with the delay
|
||||||
|
profile.onCommandFromHandler(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).sendCommand(OnOffType.ON);
|
||||||
|
verify(mockFuture, never()).cancel(anyBoolean());
|
||||||
|
|
||||||
|
// the pending OFF still fires after its delay
|
||||||
|
job.run();
|
||||||
|
verify(mockCallback, times(1)).sendUpdate(OnOffType.OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testItemEventsPassThrough() {
|
||||||
|
StateProfile profile = initProfile(0, 5000);
|
||||||
|
profile.onCommandFromItem(OnOffType.ON);
|
||||||
|
verify(mockCallback, times(1)).handleCommand(OnOffType.ON);
|
||||||
|
profile.onStateUpdateFromItem(OnOffType.ON);
|
||||||
|
verify(mockCallback, never()).sendUpdate(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user