Skip to content

Commit e4cc912

Browse files
authored
Make expire support days, ISO8601 Duration and fully configurable through metadata configuration map (openhab#4724)
* Make `expire` fully configurable through metadata configuration map * raise an error when setting is specified in both value and config map * support days and ISO8601 * unknown config raises an error * throws an exception on negative duration Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent 9e7aa7b commit e4cc912

File tree

2 files changed

+194
-35
lines changed

2 files changed

+194
-35
lines changed

bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.time.Duration;
1616
import java.time.Instant;
17+
import java.util.HashSet;
1718
import java.util.Map;
1819
import java.util.Optional;
1920
import java.util.Set;
@@ -289,19 +290,24 @@ public void updated(Metadata oldElement, Metadata element) {
289290
}
290291

291292
static class ExpireConfig {
293+
static final String CONFIG_DURATION = "duration";
294+
static final String CONFIG_COMMAND = "command";
295+
static final String CONFIG_STATE = "state";
292296
static final String CONFIG_IGNORE_STATE_UPDATES = "ignoreStateUpdates";
293297
static final String CONFIG_IGNORE_COMMANDS = "ignoreCommands";
298+
static final Set<String> CONFIG_KEYS = Set.of(CONFIG_DURATION, CONFIG_COMMAND, CONFIG_STATE,
299+
CONFIG_IGNORE_STATE_UPDATES, CONFIG_IGNORE_COMMANDS);
294300

295301
private static final StringType STRING_TYPE_NULL_HYPHEN = new StringType("'NULL'");
296302
private static final StringType STRING_TYPE_NULL = new StringType("NULL");
297303
private static final StringType STRING_TYPE_UNDEF_HYPHEN = new StringType("'UNDEF'");
298304
private static final StringType STRING_TYPE_UNDEF = new StringType("UNDEF");
299305

300-
protected static final String COMMAND_PREFIX = "command=";
301-
protected static final String STATE_PREFIX = "state=";
306+
protected static final String COMMAND_PREFIX = CONFIG_COMMAND + "=";
307+
protected static final String STATE_PREFIX = CONFIG_STATE + "=";
302308

303-
protected static final Pattern DURATION_PATTERN = Pattern
304-
.compile("(?:([0-9]+)H)?\\s*(?:([0-9]+)M)?\\s*(?:([0-9]+)S)?", Pattern.CASE_INSENSITIVE);
309+
protected static final Pattern DURATION_PATTERN = Pattern.compile(
310+
"(?:([0-9]+)D)?\\s*(?:([0-9]+)H)?\\s*(?:([0-9]+)M)?\\s*(?:([0-9]+)S)?", Pattern.CASE_INSENSITIVE);
305311

306312
final @Nullable Command expireCommand;
307313
final @Nullable State expireState;
@@ -315,19 +321,48 @@ static class ExpireConfig {
315321
*
316322
* Valid syntax:
317323
*
318-
* {@code &lt;duration&gt;[,(state=|command=|)&lt;stateOrCommand&gt;][,ignoreStateUpdates][,ignoreCommands]}<br>
324+
* {@code &lt;duration&gt;[,(state=|command=|)&lt;stateOrCommand&gt;]}<br>
319325
* if neither state= or command= is present, assume state
326+
*
327+
* {@code duration} is a string of the form "1d1h15m30s" or "1d" or "1h" or "15m" or "30s",
328+
* or an ISO-8601 duration string (e.g. "PT1H15M30S").
329+
*
330+
* {@code configuration} is a map of configuration keys and values:
331+
* - {@code duration}: the duration string
332+
* - {@code command}: the {@link Command} to send when the item expires
333+
* - {@code state}: the {@link State} to send when the item expires
334+
* - {@code ignoreStateUpdates}: if true, ignore state updates
335+
* - {@code ignoreCommands}: if true, ignore commands
336+
*
337+
* - When neither command nor state is specified, the default is to post an {@link UNDEF} state.
320338
*
321339
* @param item the item to which we are bound
322-
* @param configString the string that the user specified in the metadate
323-
* @throws IllegalArgumentException if it is ill-formatted
340+
* @param configString the string that the user specified in the metadata
341+
* @param configuration the configuration map
342+
* @throws IllegalArgumentException if it is ill-formatted, or the configuration contains an unknown key,
343+
* or any setting is specified more than once
324344
*/
325345
public ExpireConfig(Item item, String configString, Map<String, Object> configuration)
326346
throws IllegalArgumentException {
327347
int commaPos = configString.indexOf(',');
348+
String commandString = null;
349+
String stateString = null;
350+
351+
String durationStr = (commaPos >= 0) ? configString.substring(0, commaPos).trim() : configString.trim();
352+
if (configuration.containsKey(CONFIG_DURATION)) {
353+
if (!durationStr.isEmpty()) {
354+
throw new IllegalArgumentException("Expire duration for item " + item.getName()
355+
+ " is specified in both the value string and the configuration");
356+
}
357+
durationStr = configuration.get(CONFIG_DURATION).toString();
358+
}
328359

329-
durationString = (commaPos >= 0) ? configString.substring(0, commaPos).trim() : configString.trim();
360+
durationString = durationStr;
330361
duration = parseDuration(durationString);
362+
if (duration.isNegative()) {
363+
throw new IllegalArgumentException(
364+
"Expire duration for item " + item.getName() + " must be a positive value");
365+
}
331366

332367
String stateOrCommand = ((commaPos >= 0) && (configString.length() - 1) > commaPos)
333368
? configString.substring(commaPos + 1).trim()
@@ -336,40 +371,70 @@ public ExpireConfig(Item item, String configString, Map<String, Object> configur
336371
ignoreStateUpdates = getBooleanConfigValue(configuration, CONFIG_IGNORE_STATE_UPDATES);
337372
ignoreCommands = getBooleanConfigValue(configuration, CONFIG_IGNORE_COMMANDS);
338373

374+
if (configuration.containsKey(CONFIG_COMMAND)) {
375+
commandString = configuration.get(CONFIG_COMMAND).toString();
376+
}
377+
378+
if (configuration.containsKey(CONFIG_STATE)) {
379+
if (commandString != null) {
380+
throw new IllegalArgumentException(
381+
"Expire configuration for item " + item.getName() + " contains both command and state");
382+
}
383+
stateString = configuration.get(CONFIG_STATE).toString();
384+
}
385+
339386
if ((stateOrCommand != null) && (!stateOrCommand.isEmpty())) {
387+
if (commandString != null || stateString != null) {
388+
throw new IllegalArgumentException("Expire state/command for item " + item.getName()
389+
+ " is specified in both the value string and the configuration");
390+
}
391+
340392
if (stateOrCommand.startsWith(COMMAND_PREFIX)) {
341-
String commandString = stateOrCommand.substring(COMMAND_PREFIX.length());
342-
expireCommand = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
343-
expireState = null;
344-
if (expireCommand == null) {
345-
throw new IllegalArgumentException("The string '" + commandString
346-
+ "' does not represent a valid command for item " + item.getName());
347-
}
393+
commandString = stateOrCommand.substring(COMMAND_PREFIX.length());
348394
} else {
349395
if (stateOrCommand.startsWith(STATE_PREFIX)) {
350396
stateOrCommand = stateOrCommand.substring(STATE_PREFIX.length());
351397
}
352-
String stateString = stateOrCommand;
353-
State state = TypeParser.parseState(item.getAcceptedDataTypes(), stateString);
354-
// do special handling to allow NULL and UNDEF as strings when being put in single quotes
355-
if (STRING_TYPE_NULL_HYPHEN.equals(state)) {
356-
expireState = STRING_TYPE_NULL;
357-
} else if (STRING_TYPE_UNDEF_HYPHEN.equals(state)) {
358-
expireState = STRING_TYPE_UNDEF;
359-
} else {
360-
expireState = state;
361-
}
362-
expireCommand = null;
363-
if (expireState == null) {
364-
throw new IllegalArgumentException("The string '" + stateString
365-
+ "' does not represent a valid state for item " + item.getName());
366-
}
398+
stateString = stateOrCommand;
399+
}
400+
}
401+
402+
if (commandString != null) {
403+
expireCommand = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
404+
expireState = null;
405+
if (expireCommand == null) {
406+
throw new IllegalArgumentException("The string '" + commandString
407+
+ "' does not represent a valid command for item " + item.getName());
408+
}
409+
} else if (stateString != null) {
410+
// default is to post state
411+
expireCommand = null;
412+
State state = TypeParser.parseState(item.getAcceptedDataTypes(), stateString);
413+
// do special handling to allow NULL and UNDEF as strings when being put in single quotes
414+
if (STRING_TYPE_NULL_HYPHEN.equals(state)) {
415+
expireState = STRING_TYPE_NULL;
416+
} else if (STRING_TYPE_UNDEF_HYPHEN.equals(state)) {
417+
expireState = STRING_TYPE_UNDEF;
418+
} else {
419+
expireState = state;
420+
}
421+
422+
if (expireState == null) {
423+
throw new IllegalArgumentException("The string '" + stateString
424+
+ "' does not represent a valid state for item " + item.getName());
367425
}
368426
} else {
369427
// default is to post Undefined state
370428
expireCommand = null;
371429
expireState = UnDefType.UNDEF;
372430
}
431+
432+
if (!CONFIG_KEYS.containsAll(configuration.keySet())) {
433+
Set<String> unknownKeys = new HashSet<String>(configuration.keySet());
434+
unknownKeys.removeAll(CONFIG_KEYS);
435+
throw new IllegalArgumentException(
436+
"Expire configuration for item " + item.getName() + " contains unknown keys: " + unknownKeys);
437+
}
373438
}
374439

375440
/**
@@ -394,21 +459,31 @@ private boolean getBooleanConfigValue(Map<String, Object> configuration, String
394459
}
395460

396461
private Duration parseDuration(String durationString) throws IllegalArgumentException {
462+
try {
463+
return Duration.parse(durationString);
464+
} catch (Exception e) {
465+
// ignore
466+
}
467+
397468
Matcher m = DURATION_PATTERN.matcher(durationString);
398-
if (!m.matches() || (m.group(1) == null && m.group(2) == null && m.group(3) == null)) {
469+
if (!m.matches()
470+
|| (m.group(1) == null && m.group(2) == null && m.group(3) == null && m.group(4) == null)) {
399471
throw new IllegalArgumentException(
400-
"Invalid duration: " + durationString + ". Expected something like: '1h 15m 30s'");
472+
"Invalid duration: " + durationString + ". Expected something like: '1d 1h 15m 30s'");
401473
}
402474

403475
Duration duration = Duration.ZERO;
404476
if (m.group(1) != null) {
405-
duration = duration.plus(Duration.ofHours(Long.parseLong(m.group(1))));
477+
duration = duration.plus(Duration.ofDays(Long.parseLong(m.group(1))));
406478
}
407479
if (m.group(2) != null) {
408-
duration = duration.plus(Duration.ofMinutes(Long.parseLong(m.group(2))));
480+
duration = duration.plus(Duration.ofHours(Long.parseLong(m.group(2))));
409481
}
410482
if (m.group(3) != null) {
411-
duration = duration.plus(Duration.ofSeconds(Long.parseLong(m.group(3))));
483+
duration = duration.plus(Duration.ofMinutes(Long.parseLong(m.group(3))));
484+
}
485+
if (m.group(4) != null) {
486+
duration = duration.plus(Duration.ofSeconds(Long.parseLong(m.group(4))));
412487
}
413488
return duration;
414489
}

bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,41 @@ void testExpireConfig() {
277277
assertFalse(cfg.ignoreStateUpdates);
278278
assertFalse(cfg.ignoreCommands);
279279

280+
cfg = new ExpireManager.ExpireConfig(testItem, "7d 5h 3m 2s", Map.of());
281+
assertEquals(Duration.ofDays(7).plusHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
282+
assertEquals(UnDefType.UNDEF, cfg.expireState);
283+
assertNull(cfg.expireCommand);
284+
assertFalse(cfg.ignoreStateUpdates);
285+
assertFalse(cfg.ignoreCommands);
286+
287+
cfg = new ExpireManager.ExpireConfig(testItem, "PT5H3M2S", Map.of());
288+
assertEquals(Duration.ofHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
289+
assertEquals(UnDefType.UNDEF, cfg.expireState);
290+
assertNull(cfg.expireCommand);
291+
assertFalse(cfg.ignoreStateUpdates);
292+
assertFalse(cfg.ignoreCommands);
293+
294+
cfg = new ExpireManager.ExpireConfig(testItem, "P7DT5H3M2S", Map.of());
295+
assertEquals(Duration.ofDays(7).plusHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
296+
assertEquals(UnDefType.UNDEF, cfg.expireState);
297+
assertNull(cfg.expireCommand);
298+
assertFalse(cfg.ignoreStateUpdates);
299+
assertFalse(cfg.ignoreCommands);
300+
301+
cfg = new ExpireManager.ExpireConfig(testItem, "", Map.of("duration", "5h 3m 2s"));
302+
assertEquals(Duration.ofHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
303+
assertEquals(UnDefType.UNDEF, cfg.expireState);
304+
assertNull(cfg.expireCommand);
305+
assertFalse(cfg.ignoreStateUpdates);
306+
assertFalse(cfg.ignoreCommands);
307+
308+
cfg = new ExpireManager.ExpireConfig(testItem, "", Map.of("duration", "PT5H3M2S"));
309+
assertEquals(Duration.ofHours(5).plusMinutes(3).plusSeconds(2), cfg.duration);
310+
assertEquals(UnDefType.UNDEF, cfg.expireState);
311+
assertNull(cfg.expireCommand);
312+
assertFalse(cfg.ignoreStateUpdates);
313+
assertFalse(cfg.ignoreCommands);
314+
280315
cfg = new ExpireManager.ExpireConfig(testItem, "1h,OFF", Map.of());
281316
assertEquals(Duration.ofHours(1), cfg.duration);
282317
assertEquals(OnOffType.OFF, cfg.expireState);
@@ -291,13 +326,27 @@ void testExpireConfig() {
291326
assertFalse(cfg.ignoreStateUpdates);
292327
assertFalse(cfg.ignoreCommands);
293328

329+
cfg = new ExpireManager.ExpireConfig(testItem, "1h", Map.of("state", "OFF"));
330+
assertEquals(Duration.ofHours(1), cfg.duration);
331+
assertEquals(OnOffType.OFF, cfg.expireState);
332+
assertNull(cfg.expireCommand);
333+
assertFalse(cfg.ignoreStateUpdates);
334+
assertFalse(cfg.ignoreCommands);
335+
294336
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OFF", Map.of());
295337
assertEquals(Duration.ofHours(1), cfg.duration);
296338
assertNull(cfg.expireState);
297339
assertEquals(OnOffType.OFF, cfg.expireCommand);
298340
assertFalse(cfg.ignoreStateUpdates);
299341
assertFalse(cfg.ignoreCommands);
300342

343+
cfg = new ExpireManager.ExpireConfig(testItem, "1h", Map.of("command", "OFF"));
344+
assertEquals(Duration.ofHours(1), cfg.duration);
345+
assertNull(cfg.expireState);
346+
assertEquals(OnOffType.OFF, cfg.expireCommand);
347+
assertFalse(cfg.ignoreStateUpdates);
348+
assertFalse(cfg.ignoreCommands);
349+
301350
cfg = new ExpireManager.ExpireConfig(testItem, "1h,command=OFF",
302351
Map.of(ExpireConfig.CONFIG_IGNORE_STATE_UPDATES, true));
303352
assertEquals(Duration.ofHours(1), cfg.duration);
@@ -368,6 +417,41 @@ void testExpireConfig() {
368417
assertNull(cfg.expireCommand);
369418
}
370419

420+
@Test
421+
void testValueVsConfigMix() {
422+
Item testItem = new SwitchItem(ITEMNAME);
423+
424+
assertThrows(IllegalArgumentException.class,
425+
() -> new ExpireManager.ExpireConfig(testItem, "1h", Map.of("duration", "1h")));
426+
assertThrows(IllegalArgumentException.class,
427+
() -> new ExpireManager.ExpireConfig(testItem, "1h,state=ON", Map.of("state", "ON")));
428+
assertThrows(IllegalArgumentException.class,
429+
() -> new ExpireManager.ExpireConfig(testItem, "1h,state=ON", Map.of("command", "ON")));
430+
assertThrows(IllegalArgumentException.class,
431+
() -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON", Map.of("command", "ON")));
432+
assertThrows(IllegalArgumentException.class,
433+
() -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON", Map.of("state", "ON")));
434+
assertThrows(IllegalArgumentException.class, //
435+
() -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON",
436+
Map.of("command", "ON", "state", "ON")));
437+
assertThrows(IllegalArgumentException.class,
438+
() -> new ExpireManager.ExpireConfig(testItem, "1h", Map.of("command", "ON", "state", "ON")));
439+
}
440+
441+
@Test
442+
void testUnknownConfigKeys() {
443+
Item testItem = new SwitchItem(ITEMNAME);
444+
assertThrows(IllegalArgumentException.class,
445+
() -> new ExpireManager.ExpireConfig(testItem, "1h", Map.of("unknownKey", "unknownValue")));
446+
}
447+
448+
@Test
449+
void testNegativeDuration() {
450+
Item testItem = new SwitchItem(ITEMNAME);
451+
assertThrows(IllegalArgumentException.class, () -> new ExpireManager.ExpireConfig(testItem, "-1h", Map.of()));
452+
assertThrows(IllegalArgumentException.class, () -> new ExpireManager.ExpireConfig(testItem, "-PT1H", Map.of()));
453+
}
454+
371455
private Metadata config(String metadata) {
372456
return new Metadata(METADATA_KEY, metadata, null);
373457
}

0 commit comments

Comments
 (0)