Skip to content

Commit

Permalink
[expiry] extend ExpiryManager to allow ignoring state updates (openha…
Browse files Browse the repository at this point in the history
…b#2739)

* [expiry] extend ExpiryManager to allow ignoring state updates

Signed-off-by: Jan N. Klug <github@klug.nrw>
  • Loading branch information
J-N-K authored Feb 24, 2022
1 parent e420cf1 commit 2565b5c
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -140,10 +141,13 @@ protected void deactivate() {
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;

Expand All @@ -166,14 +170,14 @@ private void processEvent(String itemName, Type type) {
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) {
Expand Down Expand Up @@ -240,13 +244,13 @@ public void receive(Event event) {
}
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);
}
}

Expand Down Expand Up @@ -284,6 +288,7 @@ public void updated(Metadata oldElement, Metadata element) {
}

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");
Expand All @@ -296,26 +301,26 @@ static class ExpireConfig {
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 &lt;duration&gt;[,[state=|command=|]&lt;stateOrCommand&gt;]}<br>
* {@code &lt;duration&gt;[,(state=|command=|)&lt;stateOrCommand&gt;][,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();
Expand All @@ -325,6 +330,15 @@ public ExpireConfig(Item item, String configString) throws IllegalArgumentExcept
? 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());
Expand Down Expand Up @@ -384,7 +398,7 @@ private Duration parseDuration(String durationString) throws IllegalArgumentExce
@Override
public String toString() {
return "duration='" + durationString + "', s=" + duration.toSeconds() + ", state='" + expireState
+ "', command='" + expireCommand + "'";
+ "', command='" + expireCommand + "', ignoreStateUpdates=" + ignoreStateUpdates;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import javax.measure.quantity.Temperature;

Expand All @@ -40,6 +41,7 @@
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;
Expand Down Expand Up @@ -128,6 +130,58 @@ void testCancelExpiry() throws InterruptedException, ItemNotFoundException {
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);
Expand Down Expand Up @@ -155,69 +209,92 @@ void testMetadataChange() throws InterruptedException, ItemNotFoundException {
@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);
}
}

0 comments on commit 2565b5c

Please sign in to comment.