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..0881ff75f23 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 @@ -24,9 +24,11 @@ */ 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); + public EnrichedGroupItemDTO(ItemDTO itemDTO, EnrichedItemDTO[] members, String link, String state, String lastState, + Long lastStateUpdate, Long lastStateChange, String transformedState, StateDescription stateDescription, + String unitSymbol) { + super(itemDTO, link, state, lastState, lastStateUpdate, lastStateChange, 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..a7177a5e5d0 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 lastState; + public Long lastStateUpdate; + public Long lastStateChange; + 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 lastState, Long lastStateUpdate, + Long lastStateChange, 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.lastState = lastState; + this.lastStateUpdate = lastStateUpdate; + this.lastStateChange = lastStateChange; 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..dd41bfa8e54 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 lastState = Optional.ofNullable(item.getLastState()).map(State::toFullString).orElse(null); + Long lastStateUpdate = Optional.ofNullable(item.getLastStateUpdate()).map(zdt -> zdt.toInstant().toEpochMilli()) + .orElse(null); + Long lastStateChange = Optional.ofNullable(item.getLastStateChange()).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, lastState, lastStateUpdate, + lastStateChange, transformedState, stateDescription, unitSymbol); } else { - enrichedItemDTO = new EnrichedItemDTO(itemDTO, link, state, transformedState, stateDescription, - item.getCommandDescription(locale), unitSymbol); + enrichedItemDTO = new EnrichedItemDTO(itemDTO, link, state, lastState, lastStateUpdate, lastStateChange, + 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..d7a3ba5e12d 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 lastState; + + protected @Nullable ZonedDateTime lastStateUpdate; + protected @Nullable ZonedDateTime lastStateChange; protected @Nullable String label; @@ -103,6 +108,21 @@ public State getState() { return state.as(typeClass); } + @Override + public @Nullable State getLastState() { + return lastState; + } + + @Override + public @Nullable ZonedDateTime getLastStateUpdate() { + return lastStateUpdate; + } + + @Override + public @Nullable ZonedDateTime getLastStateChange() { + return lastStateChange; + } + @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) { + lastState = oldState; // update before we notify listeners + } notifyListeners(oldState, state); sendStateUpdatedEvent(state); - if (!oldState.equals(state)) { + if (stateChanged) { sendStateChangedEvent(state, oldState); + lastStateChange = now; // update after we've notified listeners } + lastStateUpdate = 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..8d71b860eaa 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 getLastState(); + + /** + * 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 getLastStateUpdate(); + + /** + * 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 getLastStateChange(); + /** * 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..f7226ba49a2 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 testGetLastStateUpdate() { + TestItem item = new TestItem("member1"); + assertNull(item.getLastStateUpdate()); + item.setState(PercentType.HUNDRED); + assertThat(item.getLastStateUpdate().toInstant().toEpochMilli() * 1.0, + is(closeTo(ZonedDateTime.now().toInstant().toEpochMilli(), 5))); + } + + @Test + public void testGetLastStateChange() throws InterruptedException { + TestItem item = new TestItem("member1"); + assertNull(item.getLastStateChange()); + item.setState(PercentType.HUNDRED); + ZonedDateTime initialChangeTime = ZonedDateTime.now(); + assertThat(item.getLastStateChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(initialChangeTime.toInstant().toEpochMilli(), 5))); + + Thread.sleep(50); + item.setState(PercentType.HUNDRED); + assertThat(item.getLastStateChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(initialChangeTime.toInstant().toEpochMilli(), 5))); + + Thread.sleep(50); + ZonedDateTime secondChangeTime = ZonedDateTime.now(); + item.setState(PercentType.ZERO); + assertThat(item.getLastStateChange().toInstant().toEpochMilli() * 1.0, + is(closeTo(secondChangeTime.toInstant().toEpochMilli(), 5))); + } + + @Test + public void testGetLastState() { + TestItem item = new TestItem("member1"); + assertEquals(UnDefType.NULL, item.getState()); + assertNull(item.getLastState()); + item.setState(PercentType.HUNDRED); + assertEquals(UnDefType.NULL, item.getLastState()); + item.setState(PercentType.ZERO); + assertEquals(PercentType.HUNDRED, item.getLastState()); + } + @Test public void testDispose() { TestItem item = new TestItem("test");