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..a5dc68a4a20 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,33 @@ public State getState() { return state.as(typeClass); } + /** + * Returns the previous state of the item. + * + * @return the previous state of the item, or null if the item has never been changed. + */ + public @Nullable State getPreviousState() { + return previousState; + } + + /** + * 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. + */ + public @Nullable ZonedDateTime getLastUpdate() { + return lastUpdate; + } + + /** + * 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. + */ + public @Nullable ZonedDateTime getLastChange() { + return lastChange; + } + @Override public String getUID() { return getName(); @@ -218,13 +250,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/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");