mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
[expiry] extend ExpiryManager to allow ignoring state updates (#2739)
* [expiry] extend ExpiryManager to allow ignoring state updates Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
+35
-21
@@ -41,6 +41,7 @@ import org.openhab.core.items.MetadataRegistry;
|
||||
import org.openhab.core.items.events.GroupItemStateChangedEvent;
|
||||
import org.openhab.core.items.events.ItemCommandEvent;
|
||||
import org.openhab.core.items.events.ItemEventFactory;
|
||||
import org.openhab.core.items.events.ItemStateChangedEvent;
|
||||
import org.openhab.core.items.events.ItemStateEvent;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
@@ -72,15 +73,15 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
protected static final String METADATA_NAMESPACE = "expire";
|
||||
protected static final String PROPERTY_ENABLED = "enabled";
|
||||
|
||||
private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE,
|
||||
GroupItemStateChangedEvent.TYPE);
|
||||
private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE,
|
||||
ItemCommandEvent.TYPE, GroupItemStateChangedEvent.TYPE);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ExpireManager.class);
|
||||
|
||||
private Map<String, @Nullable Optional<ExpireConfig>> itemExpireConfig = new ConcurrentHashMap<>();
|
||||
private Map<String, Instant> itemExpireMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, Optional<ExpireConfig>> itemExpireConfig = new ConcurrentHashMap<>();
|
||||
private final Map<String, Instant> itemExpireMap = new ConcurrentHashMap<>();
|
||||
|
||||
private ScheduledExecutorService threadPool = ThreadPoolManager
|
||||
private final ScheduledExecutorService threadPool = ThreadPoolManager
|
||||
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
|
||||
|
||||
private final EventPublisher eventPublisher;
|
||||
@@ -140,10 +141,13 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
itemExpireMap.clear();
|
||||
}
|
||||
|
||||
private void processEvent(String itemName, Type type) {
|
||||
logger.trace("Received '{}' for item {}", type, itemName);
|
||||
private void processEvent(String itemName, Type type, boolean isStateUpdate) {
|
||||
logger.trace("Received '{}' for item {}, state update = {}", type, itemName, isStateUpdate);
|
||||
ExpireConfig expireConfig = getExpireConfig(itemName);
|
||||
if (expireConfig != null) {
|
||||
if (isStateUpdate && expireConfig.ignoreStateUpdates) {
|
||||
return;
|
||||
}
|
||||
Command expireCommand = expireConfig.expireCommand;
|
||||
State expireState = expireConfig.expireState;
|
||||
|
||||
@@ -166,14 +170,14 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
private @Nullable ExpireConfig getExpireConfig(String itemName) {
|
||||
Optional<ExpireConfig> itemConfig = itemExpireConfig.get(itemName);
|
||||
if (itemConfig != null) {
|
||||
return itemConfig.isPresent() ? itemConfig.get() : null;
|
||||
return itemConfig.orElse(null);
|
||||
} else {
|
||||
Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_NAMESPACE, itemName));
|
||||
if (metadata != null) {
|
||||
try {
|
||||
Item item = itemRegistry.getItem(itemName);
|
||||
try {
|
||||
ExpireConfig cfg = new ExpireConfig(item, metadata.getValue());
|
||||
ExpireConfig cfg = new ExpireConfig(item, metadata.getValue(), metadata.getConfiguration());
|
||||
itemExpireConfig.put(itemName, Optional.of(cfg));
|
||||
return cfg;
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -240,13 +244,13 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
}
|
||||
if (event instanceof ItemStateEvent) {
|
||||
ItemStateEvent isEvent = (ItemStateEvent) event;
|
||||
processEvent(isEvent.getItemName(), isEvent.getItemState());
|
||||
processEvent(isEvent.getItemName(), isEvent.getItemState(), true);
|
||||
} else if (event instanceof ItemCommandEvent) {
|
||||
ItemCommandEvent icEvent = (ItemCommandEvent) event;
|
||||
processEvent(icEvent.getItemName(), icEvent.getItemCommand());
|
||||
} else if (event instanceof GroupItemStateChangedEvent) {
|
||||
GroupItemStateChangedEvent gcEvent = (GroupItemStateChangedEvent) event;
|
||||
processEvent(gcEvent.getItemName(), gcEvent.getItemState());
|
||||
processEvent(icEvent.getItemName(), icEvent.getItemCommand(), false);
|
||||
} else if (event instanceof ItemStateChangedEvent) {
|
||||
ItemStateChangedEvent icEvent = (ItemStateChangedEvent) event;
|
||||
processEvent(icEvent.getItemName(), icEvent.getItemState(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +288,7 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
}
|
||||
|
||||
static class ExpireConfig {
|
||||
static final String CONFIG_IGNORE_STATE_UPDATES = "ignoreStateUpdates";
|
||||
|
||||
private static final StringType STRING_TYPE_NULL_HYPHEN = new StringType("'NULL'");
|
||||
private static final StringType STRING_TYPE_NULL = new StringType("NULL");
|
||||
@@ -296,26 +301,26 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
protected static final Pattern DURATION_PATTERN = Pattern
|
||||
.compile("(?:([0-9]+)H)?\\s*(?:([0-9]+)M)?\\s*(?:([0-9]+)S)?", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Nullable
|
||||
final Command expireCommand;
|
||||
@Nullable
|
||||
final State expireState;
|
||||
final @Nullable Command expireCommand;
|
||||
final @Nullable State expireState;
|
||||
final String durationString;
|
||||
final Duration duration;
|
||||
final boolean ignoreStateUpdates;
|
||||
|
||||
/**
|
||||
* Construct an ExpireConfig from the config string.
|
||||
*
|
||||
* Valid syntax:
|
||||
*
|
||||
* {@code <duration>[,[state=|command=|]<stateOrCommand>]}<br>
|
||||
* {@code <duration>[,(state=|command=|)<stateOrCommand>][,ignoreStateUpdates]}<br>
|
||||
* if neither state= or command= is present, assume state
|
||||
*
|
||||
* @param item the item to which we are bound
|
||||
* @param configString the string that the user specified in the metadate
|
||||
* @throws IllegalArgumentException if it is ill-formatted
|
||||
*/
|
||||
public ExpireConfig(Item item, String configString) throws IllegalArgumentException {
|
||||
public ExpireConfig(Item item, String configString, Map<String, Object> configuration)
|
||||
throws IllegalArgumentException {
|
||||
int commaPos = configString.indexOf(',');
|
||||
|
||||
durationString = (commaPos >= 0) ? configString.substring(0, commaPos).trim() : configString.trim();
|
||||
@@ -325,6 +330,15 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
? configString.substring(commaPos + 1).trim()
|
||||
: null;
|
||||
|
||||
Object ignoreStateUpdatesConfigObject = configuration.get(CONFIG_IGNORE_STATE_UPDATES);
|
||||
if (ignoreStateUpdatesConfigObject instanceof String) {
|
||||
ignoreStateUpdates = Boolean.parseBoolean((String) ignoreStateUpdatesConfigObject);
|
||||
} else if (ignoreStateUpdatesConfigObject instanceof Boolean) {
|
||||
ignoreStateUpdates = (Boolean) ignoreStateUpdatesConfigObject;
|
||||
} else {
|
||||
ignoreStateUpdates = false;
|
||||
}
|
||||
|
||||
if ((stateOrCommand != null) && (stateOrCommand.length() > 0)) {
|
||||
if (stateOrCommand.startsWith(COMMAND_PREFIX)) {
|
||||
String commandString = stateOrCommand.substring(COMMAND_PREFIX.length());
|
||||
@@ -384,7 +398,7 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener<It
|
||||
@Override
|
||||
public String toString() {
|
||||
return "duration='" + durationString + "', s=" + duration.toSeconds() + ", state='" + expireState
|
||||
+ "', command='" + expireCommand + "'";
|
||||
+ "', command='" + expireCommand + "', ignoreStateUpdates=" + ignoreStateUpdates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+95
-18
@@ -18,6 +18,7 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
@@ -40,6 +41,7 @@ import org.openhab.core.items.events.ItemEventFactory;
|
||||
import org.openhab.core.library.items.NumberItem;
|
||||
import org.openhab.core.library.items.StringItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
@@ -128,6 +130,58 @@ class ExpireManagerTest {
|
||||
verify(eventPublisherMock, never()).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIgnoreStateUpdateExtendsExpiryOnStateChange() throws InterruptedException, ItemNotFoundException {
|
||||
Item testItem = new NumberItem(ITEMNAME);
|
||||
when(itemRegistryMock.getItem(ITEMNAME)).thenReturn(testItem);
|
||||
when(metadataRegistryMock.get(METADATA_KEY))
|
||||
.thenReturn(config("2s", Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, true)));
|
||||
|
||||
Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
event = ItemEventFactory.createStateChangedEvent(ITEMNAME, new DecimalType(2), new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
verify(eventPublisherMock, never()).post(any());
|
||||
Thread.sleep(2000L);
|
||||
verify(eventPublisherMock, times(1)).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIgnoreStateUpdateExtendsExpiryOnCommand() throws InterruptedException, ItemNotFoundException {
|
||||
Item testItem = new NumberItem(ITEMNAME);
|
||||
when(itemRegistryMock.getItem(ITEMNAME)).thenReturn(testItem);
|
||||
when(metadataRegistryMock.get(METADATA_KEY))
|
||||
.thenReturn(config("2s", Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, true)));
|
||||
|
||||
Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
verify(eventPublisherMock, never()).post(any());
|
||||
Thread.sleep(2000L);
|
||||
verify(eventPublisherMock, times(1)).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIgnoreStateUpdateDoesNotExtendExpiryOnStateUpdate() throws InterruptedException, ItemNotFoundException {
|
||||
Item testItem = new NumberItem(ITEMNAME);
|
||||
when(itemRegistryMock.getItem(ITEMNAME)).thenReturn(testItem);
|
||||
when(metadataRegistryMock.get(METADATA_KEY))
|
||||
.thenReturn(config("2s", Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, true)));
|
||||
|
||||
Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
event = ItemEventFactory.createStateEvent(ITEMNAME, new DecimalType(1));
|
||||
expireManager.receive(event);
|
||||
Thread.sleep(1500L);
|
||||
verify(eventPublisherMock, times(1)).post(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMetadataChange() throws InterruptedException, ItemNotFoundException {
|
||||
Metadata md = new Metadata(METADATA_KEY, "1s", null);
|
||||
@@ -155,69 +209,92 @@ class ExpireManagerTest {
|
||||
@Test
|
||||
void testExpireConfig() {
|
||||
Item testItem = new SwitchItem(ITEMNAME);
|
||||
ExpireConfig cfg = new ExpireManager.ExpireConfig(testItem, "1s");
|
||||
ExpireConfig cfg = new ExpireManager.ExpireConfig(testItem, "1s", Map.of());
|
||||
assertEquals(Duration.ofSeconds(1), cfg.duration);
|
||||
assertEquals(UnDefType.UNDEF, cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
assertNull(cfg.expireCommand);
|
||||
assertFalse(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "5h 3m 2s");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "5h 3m 2s", Map.of());
|
||||
assertEquals(Duration.ofHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
|
||||
assertEquals(UnDefType.UNDEF, cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
assertNull(cfg.expireCommand);
|
||||
assertFalse(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,OFF");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,OFF", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(OnOffType.OFF, cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
assertNull(cfg.expireCommand);
|
||||
assertFalse(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,state=OFF");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,state=OFF", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(OnOffType.OFF, cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
assertNull(cfg.expireCommand);
|
||||
assertFalse(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OFF");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OFF", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(null, cfg.expireState);
|
||||
assertNull(cfg.expireState);
|
||||
assertEquals(OnOffType.OFF, cfg.expireCommand);
|
||||
assertFalse(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OFF",
|
||||
Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, true));
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertNull(cfg.expireState);
|
||||
assertEquals(OnOffType.OFF, cfg.expireCommand);
|
||||
assertTrue(cfg.ignoreStateUpdates);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "5h 3m 2s",
|
||||
Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, "true"));
|
||||
assertEquals(Duration.ofHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
|
||||
assertEquals(UnDefType.UNDEF, cfg.expireState);
|
||||
assertNull(cfg.expireCommand);
|
||||
assertTrue(cfg.ignoreStateUpdates);
|
||||
|
||||
try {
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OPEN");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OPEN", Map.of());
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected as command is invalid
|
||||
}
|
||||
|
||||
try {
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,OPEN");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,OPEN", Map.of());
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected as state is invalid
|
||||
}
|
||||
|
||||
testItem = new NumberItem("Number:Temperature", ITEMNAME);
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,15 °C");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,15 °C", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(new QuantityType<Temperature>("15 °C"), cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
|
||||
testItem = new StringItem(ITEMNAME);
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,NULL");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,NULL", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(UnDefType.NULL, cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,'NULL'");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,'NULL'", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(new StringType("NULL"), cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,'UNDEF'");
|
||||
cfg = new ExpireManager.ExpireConfig(testItem, "1h,'UNDEF'", Map.of());
|
||||
assertEquals(Duration.ofHours(1), cfg.duration);
|
||||
assertEquals(new StringType("UNDEF"), cfg.expireState);
|
||||
assertEquals(null, cfg.expireCommand);
|
||||
}
|
||||
|
||||
private Metadata config(String cfg) {
|
||||
return new Metadata(METADATA_KEY, cfg, null);
|
||||
private Metadata config(String metadata) {
|
||||
return new Metadata(METADATA_KEY, metadata, null);
|
||||
}
|
||||
|
||||
private Metadata config(String metadata, Map<String, Object> configuration) {
|
||||
return new Metadata(METADATA_KEY, metadata, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user