mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
Add IntervalConditionHandler (#4583)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.core.automation.internal.module.handler;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.handler.BaseConditionModuleHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
|
||||
/**
|
||||
* ConditionHandler implementation for trigger interval limiting.
|
||||
*
|
||||
* @author Jimmy Tanagra - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IntervalConditionHandler extends BaseConditionModuleHandler {
|
||||
|
||||
public static final String MODULE_TYPE_ID = "timer.IntervalCondition";
|
||||
|
||||
/**
|
||||
* Constants for Config-Parameters corresponding to Definition in
|
||||
* IntervalConditionHandler.json
|
||||
*/
|
||||
public static final String CFG_MIN_INTERVAL = "minInterval";
|
||||
|
||||
/**
|
||||
* The minimum interval stored in nano seconds.
|
||||
*/
|
||||
private long minInterval;
|
||||
|
||||
private @Nullable Long lastAcceptedTime = null;
|
||||
|
||||
public IntervalConditionHandler(Condition condition) {
|
||||
super(condition);
|
||||
Configuration configuration = module.getConfiguration();
|
||||
this.minInterval = ((BigDecimal) configuration.get(CFG_MIN_INTERVAL)).longValue() * 1000000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfied(Map<String, Object> inputs) {
|
||||
long currentTime = System.nanoTime();
|
||||
if (lastAcceptedTime == null || currentTime - lastAcceptedTime >= minInterval) {
|
||||
lastAcceptedTime = currentTime;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+21
-15
@@ -49,7 +49,8 @@ public class TimerModuleHandlerFactory extends BaseModuleHandlerFactory {
|
||||
public static final String THREADPOOLNAME = "ruletimer";
|
||||
private static final Collection<String> TYPES = Arrays.asList(GenericCronTriggerHandler.MODULE_TYPE_ID,
|
||||
TimeOfDayTriggerHandler.MODULE_TYPE_ID, TimeOfDayConditionHandler.MODULE_TYPE_ID,
|
||||
DayOfWeekConditionHandler.MODULE_TYPE_ID, DateTimeTriggerHandler.MODULE_TYPE_ID);
|
||||
DayOfWeekConditionHandler.MODULE_TYPE_ID, DateTimeTriggerHandler.MODULE_TYPE_ID,
|
||||
IntervalConditionHandler.MODULE_TYPE_ID);
|
||||
|
||||
private final CronScheduler scheduler;
|
||||
private final ItemRegistry itemRegistry;
|
||||
@@ -78,21 +79,26 @@ public class TimerModuleHandlerFactory extends BaseModuleHandlerFactory {
|
||||
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
|
||||
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
|
||||
String moduleTypeUID = module.getTypeUID();
|
||||
if (GenericCronTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
|
||||
return new GenericCronTriggerHandler(trigger, scheduler);
|
||||
} else if (TimeOfDayTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
|
||||
return new TimeOfDayTriggerHandler(trigger, scheduler);
|
||||
} else if (DateTimeTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
|
||||
return new DateTimeTriggerHandler(trigger, scheduler, itemRegistry, bundleContext);
|
||||
} else if (TimeOfDayConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID)
|
||||
&& module instanceof Condition condition) {
|
||||
return new TimeOfDayConditionHandler(condition);
|
||||
} else if (DayOfWeekConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID)
|
||||
&& module instanceof Condition condition) {
|
||||
return new DayOfWeekConditionHandler(condition);
|
||||
} else {
|
||||
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
|
||||
if (module instanceof Trigger trigger) {
|
||||
switch (moduleTypeUID) {
|
||||
case GenericCronTriggerHandler.MODULE_TYPE_ID:
|
||||
return new GenericCronTriggerHandler(trigger, scheduler);
|
||||
case TimeOfDayTriggerHandler.MODULE_TYPE_ID:
|
||||
return new TimeOfDayTriggerHandler(trigger, scheduler);
|
||||
case DateTimeTriggerHandler.MODULE_TYPE_ID:
|
||||
return new DateTimeTriggerHandler(trigger, scheduler, itemRegistry, bundleContext);
|
||||
}
|
||||
} else if (module instanceof Condition condition) {
|
||||
switch (moduleTypeUID) {
|
||||
case TimeOfDayConditionHandler.MODULE_TYPE_ID:
|
||||
return new TimeOfDayConditionHandler(condition);
|
||||
case DayOfWeekConditionHandler.MODULE_TYPE_ID:
|
||||
return new DayOfWeekConditionHandler(condition);
|
||||
case IntervalConditionHandler.MODULE_TYPE_ID:
|
||||
return new IntervalConditionHandler(condition);
|
||||
}
|
||||
}
|
||||
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"uid": "timer.IntervalCondition",
|
||||
"label": "a minimum interval between checks is reached",
|
||||
"description": "Evaluates the interval between checks.",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "minInterval",
|
||||
"type": "INTEGER",
|
||||
"label": "Minimum Interval",
|
||||
"description": "Returns true if the last satisfied check was at least this many milliseconds ago.",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
-7
@@ -74,9 +74,9 @@ import org.slf4j.LoggerFactory;
|
||||
public abstract class BasicConditionHandlerTest extends JavaOSGiTest {
|
||||
private final Logger logger = LoggerFactory.getLogger(BasicConditionHandlerTest.class);
|
||||
private VolatileStorageService volatileStorageService = new VolatileStorageService();
|
||||
private @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
private @NonNullByDefault({}) RuleManager ruleEngine;
|
||||
private @Nullable Event itemEvent;
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
protected @NonNullByDefault({}) RuleManager ruleEngine;
|
||||
protected @Nullable Event itemEvent;
|
||||
private @NonNullByDefault({}) StartLevelService startLevelService;
|
||||
|
||||
/**
|
||||
@@ -128,7 +128,7 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertThatConditionWorksInRule() throws ItemNotFoundException {
|
||||
public void assertThatConditionWorksInRule() throws ItemNotFoundException, InterruptedException {
|
||||
String testItemName1 = "TriggeredItem";
|
||||
String testItemName2 = "SwitchedItem";
|
||||
|
||||
@@ -208,9 +208,8 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest {
|
||||
// prepare the execution
|
||||
itemEvent = null;
|
||||
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON));
|
||||
waitForAssert(() -> {
|
||||
assertThat(itemEvent, is(nullValue()));
|
||||
});
|
||||
Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing
|
||||
assertThat(itemEvent, is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.core.automation.module.timer.internal;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleManager;
|
||||
import org.openhab.core.automation.RuleStatus;
|
||||
import org.openhab.core.automation.RuleStatusInfo;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.internal.RuleEngineImpl;
|
||||
import org.openhab.core.automation.internal.module.handler.IntervalConditionHandler;
|
||||
import org.openhab.core.automation.internal.module.handler.ItemCommandActionHandler;
|
||||
import org.openhab.core.automation.internal.module.handler.ItemStateTriggerHandler;
|
||||
import org.openhab.core.automation.type.ModuleTypeRegistry;
|
||||
import org.openhab.core.automation.util.ModuleBuilder;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.events.Event;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.events.EventSubscriber;
|
||||
import org.openhab.core.items.ItemNotFoundException;
|
||||
import org.openhab.core.items.events.ItemCommandEvent;
|
||||
import org.openhab.core.items.events.ItemEventFactory;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.service.ReadyMarker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This tests the Interval Condition.
|
||||
*
|
||||
* @author Jimmy Tanagra - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
|
||||
private final Logger logger = LoggerFactory.getLogger(IntervalConditionHandlerTest.class);
|
||||
|
||||
/**
|
||||
* This checks if the condition on its own works properly.
|
||||
*/
|
||||
@Test
|
||||
public void assertThatConditionWorks() throws InterruptedException {
|
||||
// The minimum interval is 10ms
|
||||
IntervalConditionHandler handler = getIntervalConditionHandler(BigDecimal.valueOf(100));
|
||||
// First execution -> should return true
|
||||
assertThat(handler.isSatisfied(Map.of()), is(true));
|
||||
// Subsequent immediate execution -> should return false
|
||||
assertThat(handler.isSatisfied(Map.of()), is(false));
|
||||
Thread.sleep(200);
|
||||
// Execute after 200ms -> should return true
|
||||
assertThat(handler.isSatisfied(Map.of()), is(true));
|
||||
}
|
||||
|
||||
private IntervalConditionHandler getIntervalConditionHandler(BigDecimal minInterval) {
|
||||
return new IntervalConditionHandler(getIntervalCondition(minInterval));
|
||||
}
|
||||
|
||||
private Condition getIntervalCondition(BigDecimal minInterval) {
|
||||
Configuration config = getIntervalConfiguration(minInterval);
|
||||
return ModuleBuilder.createCondition().withId("testIntervalCondition")
|
||||
.withTypeUID(IntervalConditionHandler.MODULE_TYPE_ID).withConfiguration(config).build();
|
||||
}
|
||||
|
||||
private Configuration getIntervalConfiguration(BigDecimal minInterval) {
|
||||
return new Configuration(Map.of(IntervalConditionHandler.CFG_MIN_INTERVAL, minInterval));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Condition getPassingCondition() {
|
||||
return getIntervalCondition(BigDecimal.valueOf(100));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration getFailingConfiguration() {
|
||||
return getIntervalConfiguration(BigDecimal.valueOf(10000));
|
||||
}
|
||||
|
||||
// This is copied from BasicConditionHandlerTest with some modifications
|
||||
@Override
|
||||
@Test
|
||||
public void assertThatConditionWorksInRule() throws ItemNotFoundException, InterruptedException {
|
||||
String testItemName1 = "TriggeredItem";
|
||||
String testItemName2 = "SwitchedItem";
|
||||
|
||||
/*
|
||||
* Create Rule
|
||||
*/
|
||||
logger.info("Create rule");
|
||||
Configuration triggerConfig = new Configuration(Map.of("itemName", testItemName1));
|
||||
List<Trigger> triggers = List.of(ModuleBuilder.createTrigger().withId("MyTrigger")
|
||||
.withTypeUID(ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID).withConfiguration(triggerConfig).build());
|
||||
|
||||
List<Condition> conditions = List.of(getPassingCondition());
|
||||
|
||||
Map<String, Object> cfgEntries = new HashMap<>();
|
||||
cfgEntries.put("itemName", testItemName2);
|
||||
cfgEntries.put("command", "ON");
|
||||
Configuration actionConfig = new Configuration(cfgEntries);
|
||||
List<Action> actions = List.of(ModuleBuilder.createAction().withId("MyItemPostCommandAction")
|
||||
.withTypeUID(ItemCommandActionHandler.ITEM_COMMAND_ACTION).withConfiguration(actionConfig).build());
|
||||
|
||||
// prepare the execution
|
||||
EventPublisher eventPublisher = getService(EventPublisher.class);
|
||||
|
||||
// start rule engine
|
||||
RuleEngineImpl ruleEngine = Objects.requireNonNull((RuleEngineImpl) getService(RuleManager.class));
|
||||
ruleEngine.onReadyMarkerAdded(new ReadyMarker("", ""));
|
||||
waitForAssert(() -> assertTrue(ruleEngine.isStarted()));
|
||||
|
||||
EventSubscriber itemEventHandler = new EventSubscriber() {
|
||||
|
||||
@Override
|
||||
public Set<String> getSubscribedEventTypes() {
|
||||
return Set.of(ItemCommandEvent.TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Event event) {
|
||||
logger.info("Event: {}", event.getTopic());
|
||||
if (event.getTopic().contains(testItemName2)) {
|
||||
IntervalConditionHandlerTest.this.itemEvent = event;
|
||||
}
|
||||
}
|
||||
};
|
||||
registerService(itemEventHandler);
|
||||
|
||||
Rule rule = RuleBuilder.create("MyRule" + new Random().nextInt()).withTriggers(triggers)
|
||||
.withConditions(conditions).withActions(actions).withName("MyConditionTestRule").build();
|
||||
logger.info("Rule created: {}", rule.getUID());
|
||||
|
||||
logger.info("Add rule");
|
||||
ruleRegistry.add(rule);
|
||||
logger.info("Rule added");
|
||||
|
||||
logger.info("Enable rule and wait for idle status");
|
||||
ruleEngine.setEnabled(rule.getUID(), true);
|
||||
waitForAssert(() -> {
|
||||
final RuleStatusInfo ruleStatus = ruleEngine.getStatusInfo(rule.getUID());
|
||||
assertThat(ruleStatus.getStatus(), is(RuleStatus.IDLE));
|
||||
});
|
||||
logger.info("Rule is enabled and idle");
|
||||
|
||||
logger.info("Send and wait for item state is ON");
|
||||
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON));
|
||||
|
||||
// the first event is always processed
|
||||
waitForAssert(() -> {
|
||||
assertThat(itemEvent, is(notNullValue()));
|
||||
assertThat(((ItemCommandEvent) itemEvent).getItemCommand(), is(OnOffType.ON));
|
||||
});
|
||||
|
||||
long minInterval = ((BigDecimal) conditions.getFirst().getConfiguration()
|
||||
.get(IntervalConditionHandler.CFG_MIN_INTERVAL)).longValue();
|
||||
Thread.sleep(minInterval + 50);
|
||||
|
||||
// Send a second event to check if the condition is still satisfied
|
||||
itemEvent = null; // reset it
|
||||
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON));
|
||||
|
||||
waitForAssert(() -> {
|
||||
assertThat(itemEvent, is(notNullValue()));
|
||||
assertThat(((ItemCommandEvent) itemEvent).getItemCommand(), is(OnOffType.ON));
|
||||
});
|
||||
logger.info("item state is ON");
|
||||
|
||||
// now make the condition fail
|
||||
Rule rule2 = RuleBuilder.create(rule).withConditions(ModuleBuilder
|
||||
.createCondition(rule.getConditions().getFirst()).withConfiguration(getFailingConfiguration()).build())
|
||||
.build();
|
||||
ruleRegistry.update(rule2);
|
||||
|
||||
// prepare the execution
|
||||
itemEvent = null;
|
||||
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON));
|
||||
|
||||
// the first event is always allowed
|
||||
waitForAssert(() -> {
|
||||
assertThat(itemEvent, is(notNullValue()));
|
||||
});
|
||||
|
||||
Thread.sleep(200); // some time is passing but less than the failing condition's minInterval
|
||||
|
||||
// the second event is not allowed
|
||||
itemEvent = null;
|
||||
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON));
|
||||
Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing
|
||||
assertThat(itemEvent, is(nullValue()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void checkIfModuleTypeIsRegistered() {
|
||||
ModuleTypeRegistry mtr = getService(ModuleTypeRegistry.class);
|
||||
waitForAssert(() -> {
|
||||
assertThat(mtr.get(IntervalConditionHandler.MODULE_TYPE_ID), is(notNullValue()));
|
||||
}, 3000, 100);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user