diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedGroupItemDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedGroupItemDTO.java index fafc88ab2fe..d53413a4957 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedGroupItemDTO.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedGroupItemDTO.java @@ -25,8 +25,10 @@ public class EnrichedGroupItemDTO extends EnrichedItemDTO { public EnrichedGroupItemDTO(ItemDTO itemDTO, EnrichedItemDTO[] members, String link, String state, - String transformedState, StateDescription stateDescription, String unitSymbol) { - super(itemDTO, link, state, transformedState, stateDescription, null, unitSymbol); + String previousState, Long lastUpdate, Long lastChange, String transformedState, + StateDescription stateDescription, String unitSymbol) { + super(itemDTO, link, state, previousState, lastUpdate, lastChange, transformedState, stateDescription, null, + unitSymbol); this.members = members; this.groupType = ((GroupItemDTO) itemDTO).groupType; this.function = ((GroupItemDTO) itemDTO).function; diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTO.java index d1a64e7595d..69123dfbbd3 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTO.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTO.java @@ -32,13 +32,17 @@ public class EnrichedItemDTO extends ItemDTO { public String state; public String transformedState; public StateDescription stateDescription; - public String unitSymbol; public CommandDescription commandDescription; + public String previousState; + public Long lastUpdate; + public Long lastChange; + public String unitSymbol; public Map metadata; public Boolean editable; - public EnrichedItemDTO(ItemDTO itemDTO, String link, String state, String transformedState, - StateDescription stateDescription, CommandDescription commandDescription, String unitSymbol) { + public EnrichedItemDTO(ItemDTO itemDTO, String link, String state, String previousState, Long lastUpdate, + Long lastChange, String transformedState, StateDescription stateDescription, + CommandDescription commandDescription, String unitSymbol) { this.type = itemDTO.type; this.name = itemDTO.name; this.label = itemDTO.label; @@ -50,6 +54,9 @@ public EnrichedItemDTO(ItemDTO itemDTO, String link, String state, String transf this.transformedState = transformedState; this.stateDescription = stateDescription; this.commandDescription = commandDescription; + this.previousState = previousState; + this.lastUpdate = lastUpdate; + this.lastChange = lastChange; this.unitSymbol = unitSymbol; } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java index 487083e0926..4c1e71da828 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java @@ -17,6 +17,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -91,6 +92,12 @@ private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown } StateDescription stateDescription = considerTransformation(item.getStateDescription(locale)); + String previousState = Optional.ofNullable(item.getPreviousState()).map(State::toFullString).orElse(null); + Long lastUpdate = Optional.ofNullable(item.getLastUpdate()).map(zdt -> zdt.toInstant().toEpochMilli()) + .orElse(null); + Long lastChange = Optional.ofNullable(item.getLastChange()).map(zdt -> zdt.toInstant().toEpochMilli()) + .orElse(null); + final String link; if (uriBuilder != null) { link = uriBuilder.build(itemDTO.name).toASCIIString(); @@ -124,11 +131,11 @@ private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown } else { memberDTOs = new EnrichedItemDTO[0]; } - enrichedItemDTO = new EnrichedGroupItemDTO(itemDTO, memberDTOs, link, state, transformedState, - stateDescription, unitSymbol); + enrichedItemDTO = new EnrichedGroupItemDTO(itemDTO, memberDTOs, link, state, previousState, lastUpdate, + lastChange, transformedState, stateDescription, unitSymbol); } else { - enrichedItemDTO = new EnrichedItemDTO(itemDTO, link, state, transformedState, stateDescription, - item.getCommandDescription(locale), unitSymbol); + enrichedItemDTO = new EnrichedItemDTO(itemDTO, link, state, previousState, lastUpdate, lastChange, + transformedState, stateDescription, item.getCommandDescription(locale), unitSymbol); } return enrichedItemDTO; diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java index 8d36cf9de9a..48152b3c04f 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java @@ -12,6 +12,7 @@ */ package org.openhab.core.items; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -77,6 +78,10 @@ public abstract class GenericItem implements ActiveItem { protected final String type; protected State state = UnDefType.NULL; + protected @Nullable State previousState; + + protected @Nullable ZonedDateTime lastUpdate; + protected @Nullable ZonedDateTime lastChange; protected @Nullable String label; @@ -103,6 +108,21 @@ public State getState() { return state.as(typeClass); } + @Override + public @Nullable State getPreviousState() { + return previousState; + } + + @Override + public @Nullable ZonedDateTime getLastUpdate() { + return lastUpdate; + } + + @Override + public @Nullable ZonedDateTime getLastChange() { + return lastChange; + } + @Override public String getUID() { return getName(); @@ -218,13 +238,20 @@ public void setState(State state) { * @param state new state of this item */ protected final void applyState(State state) { + ZonedDateTime now = ZonedDateTime.now(); State oldState = this.state; + boolean stateChanged = !oldState.equals(state); this.state = state; + if (stateChanged) { + previousState = oldState; // update before we notify listeners + } notifyListeners(oldState, state); sendStateUpdatedEvent(state); - if (!oldState.equals(state)) { + if (stateChanged) { sendStateChangedEvent(state, oldState); + lastChange = now; // update after we've notified listeners } + lastUpdate = now; } /** diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java index 57edff58e87..393632d6c1c 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java @@ -12,6 +12,7 @@ */ package org.openhab.core.items; +import java.time.ZonedDateTime; import java.util.List; import java.util.Locale; import java.util.Set; @@ -56,6 +57,30 @@ public interface Item extends Identifiable { */ @Nullable T getStateAs(Class typeClass); + /** + * Returns the previous state of the item. + * + * @return the previous state of the item, or null if the item has never been changed. + */ + @Nullable + State getPreviousState(); + + /** + * Returns the time the item was last updated. + * + * @return the time the item was last updated, or null if the item has never been updated. + */ + @Nullable + ZonedDateTime getLastUpdate(); + + /** + * Returns the time the item was last changed. + * + * @return the time the item was last changed, or null if the item has never been changed. + */ + @Nullable + ZonedDateTime getLastChange(); + /** * returns the name of the item * diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java index d0beafcd55e..b33cdd18822 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java @@ -13,11 +13,14 @@ package org.openhab.core.items; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.time.ZonedDateTime; import java.util.List; import java.util.Locale; @@ -39,6 +42,7 @@ import org.openhab.core.types.State; import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; /** * The GenericItemTest tests functionality of the GenericItem. @@ -133,6 +137,47 @@ public void testGetStateAsWithNull() { assertNull(item.getStateAs(toNull())); } + @Test + public void testGetLastUpdate() { + TestItem item = new TestItem("member1"); + assertNull(item.getLastUpdate()); + item.setState(PercentType.HUNDRED); + assertThat(item.getLastUpdate().toInstant().toEpochMilli() * 1.0, + is(closeTo(ZonedDateTime.now().toInstant().toEpochMilli(), 5))); + } + + @Test + public void testGetLastChange() throws InterruptedException { + TestItem item = new TestItem("member1"); + assertNull(item.getLastChange()); + item.setState(PercentType.HUNDRED); + ZonedDateTime initialChangeTime = ZonedDateTime.now(); + assertThat(item.getLastChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(initialChangeTime.toInstant().toEpochMilli(), 5))); + + Thread.sleep(50); + item.setState(PercentType.HUNDRED); + assertThat(item.getLastChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(initialChangeTime.toInstant().toEpochMilli(), 5))); + + Thread.sleep(50); + ZonedDateTime secondChangeTime = ZonedDateTime.now(); + item.setState(PercentType.ZERO); + assertThat(item.getLastChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(secondChangeTime.toInstant().toEpochMilli(), 5))); + } + + @Test + public void testGetPreviousState() { + TestItem item = new TestItem("member1"); + assertEquals(UnDefType.NULL, item.getState()); + assertNull(item.getPreviousState()); + item.setState(PercentType.HUNDRED); + assertEquals(UnDefType.NULL, item.getPreviousState()); + item.setState(PercentType.ZERO); + assertEquals(PercentType.HUNDRED, item.getPreviousState()); + } + @Test public void testDispose() { TestItem item = new TestItem("test");