[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:
J-N-K
2022-02-24 21:21:23 +01:00
committed by GitHub
parent e420cf1647
commit 2565b5cbe4
2 changed files with 130 additions and 39 deletions
@@ -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 &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();
@@ -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;
}
}
}
@@ -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);
}
}