diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java index 833631f29dd..c851833f7d4 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java @@ -122,12 +122,13 @@ public void receive(Event event) { if (item != null && item.getGroupNames().contains(groupName)) { State state = isEvent.getItemState(); if ((this.state == null || state.toFullString().equals(this.state))) { - Map values = new HashMap<>(); + Map values = new HashMap<>(); if (group != null) { values.put("triggeringGroup", group); } values.put("triggeringItem", item); values.put("state", state); + values.put("lastStateUpdate", isEvent.getLastStateUpdate()); values.put("event", event); cb.triggered(this.module, values); } @@ -142,13 +143,15 @@ public void receive(Event event) { State oldState = iscEvent.getOldItemState(); if (stateMatches(this.state, state) && stateMatches(this.previousState, oldState)) { - Map values = new HashMap<>(); + Map values = new HashMap<>(); if (group != null) { values.put("triggeringGroup", group); } values.put("triggeringItem", item); values.put("oldState", oldState); values.put("newState", state); + values.put("lastStateUpdate", iscEvent.getLastStateUpdate()); + values.put("lastStateChange", iscEvent.getLastStateChange()); values.put("event", event); cb.triggered(this.module, values); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java index 20bd90d2c62..07264018c77 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java @@ -123,13 +123,14 @@ public void receive(Event event) { if (callback != null) { logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), event.getTopic(), event.getType(), event.getPayload()); - Map values = new HashMap<>(); + Map values = new HashMap<>(); if (event instanceof ItemStateUpdatedEvent updatedEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { String state = this.state; State itemState = updatedEvent.getItemState(); if ((state == null || state.equals(itemState.toFullString()))) { values.put("state", itemState); + values.put("lastStateUpdate", updatedEvent.getLastStateUpdate()); } } else if (event instanceof ItemStateChangedEvent changedEvent && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) { @@ -139,6 +140,8 @@ public void receive(Event event) { if (stateMatches(this.state, itemState) && stateMatches(this.previousState, oldItemState)) { values.put("oldState", oldItemState); values.put("newState", itemState); + values.put("lastStateUpdate", changedEvent.getLastStateUpdate()); + values.put("lastStateChange", changedEvent.getLastStateChange()); } } if (!values.isEmpty()) { diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json index fd2d41a9638..7db8597a270 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json @@ -125,6 +125,12 @@ "state" ] }, + { + "name": "lastStateUpdate", + "type": "java.time.ZonedDateTime", + "description": "the time of the previous state update", + "label": "Last State Update" + }, { "name": "event", "type": "org.openhab.core.events.Event", @@ -220,8 +226,8 @@ { "name": "newState", "type": "state", - "description": "the new item state", "label": "New State", + "description": "the new item state", "tags": [ "state" ] @@ -229,8 +235,20 @@ { "name": "oldState", "type": "state", - "description": "the old item state", - "label": "Old State" + "label": "Old State", + "description": "the old item state" + }, + { + "name": "lastStateUpdate", + "type": "java.time.ZonedDateTime", + "label": "Last State Update", + "description": "the time of the previous state update" + }, + { + "name": "lastStateChange", + "type": "java.time.ZonedDateTime", + "label": "Last State Change", + "description": "the time of the previous state change" }, { "name": "event", @@ -402,6 +420,12 @@ "state" ] }, + { + "name": "lastStateUpdate", + "type": "java.time.ZonedDateTime", + "description": "the time of the previous state update", + "label": "Last State Update" + }, { "name": "event", "type": "org.openhab.core.events.Event", @@ -518,6 +542,18 @@ "description": "the old item state", "label": "Old State" }, + { + "name": "lastStateUpdate", + "type": "java.time.ZonedDateTime", + "label": "Last State Update", + "description": "the time of the previous state update" + }, + { + "name": "lastStateChange", + "type": "java.time.ZonedDateTime", + "label": "Last State Change", + "description": "the time of the previous state change" + }, { "name": "event", "type": "org.openhab.core.events.Event", diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java index b61d06f8077..09291f4c80a 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java @@ -258,7 +258,8 @@ public void eventFromBusFilterIncludeTopic() throws IOException { eventWebSocket.processEvent(event); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); - event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO); + event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO, null, + null); eventWebSocket.processEvent(event); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); @@ -285,7 +286,8 @@ public void eventFromBusFilterExcludeTopic() throws IOException { verify(remoteEndpoint, times(0)).sendString(any()); // not excluded topics are sent - event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO); + event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO, null, + null); eventWebSocket.processEvent(event); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); @@ -309,7 +311,8 @@ public void eventFromBusFilterIncludeAndExcludeTopic() throws IOException { clearInvocations(remoteEndpoint); // included topics are sent - Event event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO); + Event event = ItemEventFactory.createStateChangedEvent(TEST_ITEM_NAME, DecimalType.ZERO, DecimalType.ZERO, null, + null); eventWebSocket.processEvent(event); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/jvmmodel/RulesJvmModelInferrer.xtend b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/jvmmodel/RulesJvmModelInferrer.xtend index 57547cfbc8d..5e0b38b8534 100644 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/jvmmodel/RulesJvmModelInferrer.xtend +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/jvmmodel/RulesJvmModelInferrer.xtend @@ -13,6 +13,7 @@ package org.openhab.core.model.rule.jvmmodel import com.google.inject.Inject +import java.time.ZonedDateTime import java.util.Set import org.openhab.core.items.Item import org.openhab.core.items.ItemRegistry @@ -145,10 +146,22 @@ class RulesJvmModelInferrer extends ScriptJvmModelInferrer { val commandTypeRef = ruleModel.newTypeRef(Command) parameters += rule.toParameter(VAR_RECEIVED_COMMAND, commandTypeRef) } + if ((containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) && !containsParam(parameters, VAR_NEW_STATE)) { + val stateTypeRef = ruleModel.newTypeRef(State) + parameters += rule.toParameter(VAR_NEW_STATE, stateTypeRef) + } if (containsStateChangeTrigger(rule) && !containsParam(parameters, VAR_PREVIOUS_STATE)) { val stateTypeRef = ruleModel.newTypeRef(State) parameters += rule.toParameter(VAR_PREVIOUS_STATE, stateTypeRef) } + if (containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) { + val lastStateUpdateTypeRef = ruleModel.newTypeRef(ZonedDateTime) + parameters += rule.toParameter(VAR_LAST_STATE_UPDATE, lastStateUpdateTypeRef) + } + if (containsStateChangeTrigger(rule)) { + val lastStateChangeTypeRef = ruleModel.newTypeRef(ZonedDateTime) + parameters += rule.toParameter(VAR_LAST_STATE_CHANGE, lastStateChangeTypeRef) + } if (containsEventTrigger(rule)) { val eventTypeRef = ruleModel.newTypeRef(String) parameters += rule.toParameter(VAR_RECEIVED_EVENT, eventTypeRef) @@ -163,10 +176,6 @@ class RulesJvmModelInferrer extends ScriptJvmModelInferrer { val newStatusRef = ruleModel.newTypeRef(String) parameters += rule.toParameter(VAR_NEW_STATUS, newStatusRef) } - if ((containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) && !containsParam(parameters, VAR_NEW_STATE)) { - val stateTypeRef = ruleModel.newTypeRef(State) - parameters += rule.toParameter(VAR_NEW_STATE, stateTypeRef) - } body = rule.script ] diff --git a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java index 296da9b22cd..457bc7fb1ef 100644 --- a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java +++ b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java @@ -61,11 +61,16 @@ public class DSLScriptEngine implements javax.script.ScriptEngine { public static final String MIMETYPE_OPENHAB_DSL_RULE = "application/vnd.openhab.dsl.rule"; - private static final Map IMPLICIT_VARS = Map.of("command", - ScriptJvmModelInferrer.VAR_RECEIVED_COMMAND, "state", ScriptJvmModelInferrer.VAR_NEW_STATE, "newState", - ScriptJvmModelInferrer.VAR_NEW_STATE, "oldState", ScriptJvmModelInferrer.VAR_PREVIOUS_STATE, - "triggeringItem", ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM, "triggeringGroup", - ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP, "input", ScriptJvmModelInferrer.VAR_INPUT); + private static final Map IMPLICIT_VARS = Map.of( // + "command", ScriptJvmModelInferrer.VAR_RECEIVED_COMMAND, // + "state", ScriptJvmModelInferrer.VAR_NEW_STATE, // + "newState", ScriptJvmModelInferrer.VAR_NEW_STATE, // + "oldState", ScriptJvmModelInferrer.VAR_PREVIOUS_STATE, // + "lastStateUpdate", ScriptJvmModelInferrer.VAR_LAST_STATE_UPDATE, // + "lastStateChange", ScriptJvmModelInferrer.VAR_LAST_STATE_CHANGE, // + "triggeringItem", ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM, // + "triggeringGroup", ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP, // + "input", ScriptJvmModelInferrer.VAR_INPUT); private final Logger logger = LoggerFactory.getLogger(DSLScriptEngine.class); diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend index 0b82b7b973a..90cf3ec02c6 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend @@ -13,6 +13,7 @@ package org.openhab.core.model.script.jvmmodel import com.google.inject.Inject +import java.time.ZonedDateTime import java.util.Set import org.openhab.core.items.ItemRegistry import org.openhab.core.model.script.scoping.StateAndCommandProvider @@ -61,6 +62,12 @@ class ScriptJvmModelInferrer extends AbstractModelInferrer { /** Variable name for the new state of an item in a "changed state triggered" or "updated state triggered" rule */ public static final String VAR_NEW_STATE = "newState"; + /** Variable name for the last update time of an item in a "changed state triggered" or "updated state triggered" rule */ + public static final String VAR_LAST_STATE_UPDATE = "lastStateUpdate"; + + /** Variable name for the last change time of an item in a "changed state triggered" rule */ + public static final String VAR_LAST_STATE_CHANGE = "lastStateChange"; + /** Variable name for the received command in a "command triggered" rule */ public static final String VAR_RECEIVED_COMMAND = "receivedCommand"; @@ -160,6 +167,10 @@ class ScriptJvmModelInferrer extends AbstractModelInferrer { parameters += script.toParameter(VAR_NEW_STATUS, newThingStatusRef) val stateTypeRef2 = script.newTypeRef(State) parameters += script.toParameter(VAR_NEW_STATE, stateTypeRef2) + val lastStateUpdateTypeRef = script.newTypeRef(ZonedDateTime) + parameters += script.toParameter(VAR_LAST_STATE_UPDATE, lastStateUpdateTypeRef) + val lastStateChangeTypeRef = script.newTypeRef(ZonedDateTime) + parameters += script.toParameter(VAR_LAST_STATE_CHANGE, lastStateChangeTypeRef) val privateCacheTypeRef = script.newTypeRef(ValueCache) parameters += script.toParameter(VAR_PRIVATE_CACHE, privateCacheTypeRef) val sharedCacheTypeRef = script.newTypeRef(ValueCache) diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java index e4286238b82..3ed26ff72e5 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java @@ -473,6 +473,9 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable int startPage = 0; filter.setPageNumber(startPage); + TimeZoneProvider tzProvider = timeZoneProvider; + ZoneId timeZone = tzProvider != null ? tzProvider.getTimeZone() : ZoneId.systemDefault(); + Iterable items = qService.query(filter, alias); while (items != null) { Iterator itemIterator = items.iterator(); @@ -484,7 +487,9 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable if (!forward && !historicItem.getState().equals(state)) { // Last persisted state value different from current state value, so it must have updated // since last persist. We do not know when from persistence, so get it from the item. - return item.getLastStateUpdate(); + return item.getLastStateUpdate() != null + ? item.getLastStateUpdate().withZoneSameInstant(timeZone) + : null; } return historicItem.getTimestamp(); } else { @@ -492,7 +497,12 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable if (!historicItem.getState().equals(state)) { // Persisted state value different from current state value, so it must have changed, but we // do not know when looking backward in persistence. Get it from the item. - return forward ? historicItem.getTimestamp() : item.getLastStateChange(); + if (forward) { + return historicItem.getTimestamp(); + } + return item.getLastStateChange() != null + ? item.getLastStateChange().withZoneSameInstant(timeZone) + : null; } while (historicItem.getState().equals(state) && itemIterator.hasNext()) { HistoricItem nextHistoricItem = itemIterator.next(); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java index 8eb7f80bffd..63d8f9e68ec 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java @@ -12,12 +12,20 @@ */ package org.openhab.core.events; +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; /** * The {@link AbstractEventFactory} defines an abstract implementation of the {@link EventFactory} interface. Subclasses @@ -31,7 +39,8 @@ public abstract class AbstractEventFactory implements EventFactory { private final Set supportedEventTypes; - private static final Gson JSONCONVERTER = new Gson(); + private static final Gson JSONCONVERTER = new GsonBuilder() + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()).create(); /** * Must be called in subclass constructor to define the supported event types. @@ -120,4 +129,25 @@ protected static void checkNotNullOrEmpty(@Nullable String string, String argume throw new IllegalArgumentException("The argument '" + argumentName + "' must not be null or empty."); } } + + public static class ZonedDateTimeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, @Nullable ZonedDateTime value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + } + + @Override + public @Nullable ZonedDateTime read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return ZonedDateTime.parse(in.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + } } 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 f91e6fb132b..24f713ed383 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 @@ -250,9 +250,9 @@ public void setState(State state, @Nullable State lastState, @Nullable ZonedDate if (oldStateUpdate != null && lastStateUpdate != null && !oldStateUpdate.equals(lastStateUpdate)) { notifyListeners(oldState, state); } - sendStateUpdatedEvent(state); + sendStateUpdatedEvent(state, lastStateUpdate); if (!oldState.equals(state)) { - sendStateChangedEvent(state, oldState); + sendStateChangedEvent(state, oldState, lastStateUpdate, lastStateChange); } } @@ -273,9 +273,9 @@ protected final void applyState(State state) { lastState = oldState; // update before we notify listeners } notifyListeners(oldState, state); - sendStateUpdatedEvent(state); + sendStateUpdatedEvent(state, lastStateUpdate); if (stateChanged) { - sendStateChangedEvent(state, oldState); + sendStateChangedEvent(state, oldState, lastStateUpdate, lastStateChange); lastStateChange = now; // update after we've notified listeners } lastStateUpdate = now; @@ -325,17 +325,19 @@ protected final void applyTimeSeries(TimeSeries timeSeries) { } } - private void sendStateUpdatedEvent(State newState) { + private void sendStateUpdatedEvent(State newState, @Nullable ZonedDateTime lastStateUpdate) { EventPublisher eventPublisher1 = this.eventPublisher; if (eventPublisher1 != null) { - eventPublisher1.post(ItemEventFactory.createStateUpdatedEvent(this.name, newState, null)); + eventPublisher1.post(ItemEventFactory.createStateUpdatedEvent(this.name, newState, lastStateUpdate, null)); } } - private void sendStateChangedEvent(State newState, State oldState) { + private void sendStateChangedEvent(State newState, State oldState, @Nullable ZonedDateTime lastStateUpdate, + @Nullable ZonedDateTime lastStateChange) { EventPublisher eventPublisher1 = this.eventPublisher; if (eventPublisher1 != null) { - eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); + eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState, + lastStateUpdate, lastStateChange)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java index 29e471d1074..c47fc3230d7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java @@ -12,6 +12,7 @@ */ package org.openhab.core.items; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -354,19 +355,22 @@ public void stateUpdated(Item item, State state) { State oldState = this.state; State newState = oldState; ItemStateConverter itemStateConverter = this.itemStateConverter; + ZonedDateTime lastStateUpdate = this.lastStateUpdate; + ZonedDateTime lastStateChange = this.lastStateChange; if (function instanceof GroupFunction groupFunction && baseItem != null && itemStateConverter != null) { State calculatedState = groupFunction.calculate(getStateMembers(getMembers())); newState = itemStateConverter.convertToAcceptedState(calculatedState, baseItem); setState(newState); - sendGroupStateUpdatedEvent(item.getName(), newState); + sendGroupStateUpdatedEvent(item.getName(), newState, lastStateUpdate); } if (!oldState.equals(newState)) { - sendGroupStateChangedEvent(item.getName(), newState, oldState); + sendGroupStateChangedEvent(item.getName(), newState, oldState, lastStateUpdate, lastStateChange); } } @Override public void setState(State state) { + ZonedDateTime now = ZonedDateTime.now(); State oldState = this.state; Item baseItem = this.baseItem; if (baseItem instanceof GenericItem item) { @@ -376,6 +380,10 @@ public void setState(State state) { this.state = state; } notifyListeners(oldState, state); + if (!oldState.equals(state)) { + lastStateChange = now; + } + lastStateUpdate = now; } @Override @@ -394,18 +402,20 @@ public void setCommandDescriptionService(@Nullable CommandDescriptionService com } } - private void sendGroupStateUpdatedEvent(String memberName, State state) { + private void sendGroupStateUpdatedEvent(String memberName, State state, @Nullable ZonedDateTime lastStateUpdate) { EventPublisher eventPublisher1 = this.eventPublisher; if (eventPublisher1 != null) { - eventPublisher1.post(ItemEventFactory.createGroupStateUpdatedEvent(getName(), memberName, state, null)); + eventPublisher1.post( + ItemEventFactory.createGroupStateUpdatedEvent(getName(), memberName, state, lastStateUpdate, null)); } } - private void sendGroupStateChangedEvent(String memberName, State newState, State oldState) { + private void sendGroupStateChangedEvent(String memberName, State newState, State oldState, + @Nullable ZonedDateTime lastStateUpdate, @Nullable ZonedDateTime lastStateChange) { EventPublisher eventPublisher1 = this.eventPublisher; if (eventPublisher1 != null) { - eventPublisher1 - .post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState, oldState)); + eventPublisher1.post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState, + oldState, lastStateUpdate, lastStateChange)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java index 5dce97cbe69..bae689126b7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java @@ -12,7 +12,10 @@ */ package org.openhab.core.items.events; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; /** @@ -34,8 +37,9 @@ public class GroupItemStateChangedEvent extends ItemStateChangedEvent { private final String memberName; protected GroupItemStateChangedEvent(String topic, String payload, String itemName, String memberName, - State newItemState, State oldItemState) { - super(topic, payload, itemName, newItemState, oldItemState); + State newItemState, State oldItemState, @Nullable ZonedDateTime lastStateUpdate, + @Nullable ZonedDateTime lastStateChange) { + super(topic, payload, itemName, newItemState, oldItemState, lastStateUpdate, lastStateChange); this.memberName = memberName; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java index 50d3db3daf2..dcba89055b0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java @@ -12,6 +12,8 @@ */ package org.openhab.core.items.events; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; @@ -34,8 +36,8 @@ public class GroupStateUpdatedEvent extends ItemStateUpdatedEvent { private final String memberName; protected GroupStateUpdatedEvent(String topic, String payload, String itemName, String memberName, - State newItemState, @Nullable String source) { - super(topic, payload, itemName, newItemState, source); + State newItemState, @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) { + super(topic, payload, itemName, newItemState, lastStateUpdate, source); this.memberName = memberName; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java index 50ea141cd98..18bd4e455b8 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java @@ -15,6 +15,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.Instant; +import java.time.ZonedDateTime; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -114,9 +115,10 @@ protected Event createEventByType(String eventType, String topic, String payload private Event createGroupStateUpdatedEvent(String topic, String payload) { String itemName = getItemName(topic); String memberName = getMemberName(topic); - ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + ItemStateUpdatedEventPayloadBean bean = deserializePayload(payload, ItemStateUpdatedEventPayloadBean.class); State state = getState(bean.getType(), bean.getValue()); - return new GroupStateUpdatedEvent(topic, payload, itemName, memberName, state, null); + ZonedDateTime lastStateUpdate = bean.getLastUpdate(); + return new GroupStateUpdatedEvent(topic, payload, itemName, memberName, state, lastStateUpdate, null); } private Event createGroupStateChangedEvent(String topic, String payload) { @@ -125,7 +127,10 @@ private Event createGroupStateChangedEvent(String topic, String payload) { ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); State state = getState(bean.getType(), bean.getValue()); State oldState = getState(bean.getOldType(), bean.getOldValue()); - return new GroupItemStateChangedEvent(topic, payload, itemName, memberName, state, oldState); + ZonedDateTime lastStateChange = bean.getLastStateChange(); + ZonedDateTime lastStateUpdate = bean.getLastStateUpdate(); + return new GroupItemStateChangedEvent(topic, payload, itemName, memberName, state, oldState, lastStateUpdate, + lastStateChange); } private Event createCommandEvent(String topic, String payload, @Nullable String source) { @@ -151,9 +156,10 @@ private Event createStatePredictedEvent(String topic, String payload) { private Event createStateUpdatedEvent(String topic, String payload) { String itemName = getItemName(topic); - ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + ItemStateUpdatedEventPayloadBean bean = deserializePayload(payload, ItemStateUpdatedEventPayloadBean.class); State state = getState(bean.getType(), bean.getValue()); - return new ItemStateUpdatedEvent(topic, payload, itemName, state, null); + ZonedDateTime lastStateUpdate = bean.getLastUpdate(); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, lastStateUpdate, null); } private Event createStateChangedEvent(String topic, String payload) { @@ -161,7 +167,9 @@ private Event createStateChangedEvent(String topic, String payload) { ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); State state = getState(bean.getType(), bean.getValue()); State oldState = getState(bean.getOldType(), bean.getOldValue()); - return new ItemStateChangedEvent(topic, payload, itemName, state, oldState); + ZonedDateTime lastStateUpdate = bean.getLastStateUpdate(); + ZonedDateTime lastStateChange = bean.getLastStateChange(); + return new ItemStateChangedEvent(topic, payload, itemName, state, oldState, lastStateUpdate, lastStateChange); } private Event createTimeSeriesEvent(String topic, String payload) { @@ -319,11 +327,13 @@ public static ItemEvent createStateEvent(String itemName, State state) { * * @param itemName the name of the item to report the state update for * @param state the new state + * @param lastStateUpdate the time of the last state update * @return the created item state update event * @throws IllegalArgumentException if itemName or state is null */ - public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state) { - return createStateUpdatedEvent(itemName, state, null); + public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state, + @Nullable ZonedDateTime lastStateUpdate) { + return createStateUpdatedEvent(itemName, state, lastStateUpdate, null); } /** @@ -331,16 +341,19 @@ public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, Sta * * @param itemName the name of the item to report the state update for * @param state the new state + * @param lastStateUpdate the time of the last state update * @param source the name of the source identifying the sender (can be null) * @return the created item state update event * @throws IllegalArgumentException if itemName or state is null */ - public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state, @Nullable String source) { + public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state, + @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) { assertValidArguments(itemName, state, "state"); String topic = buildTopic(ITEM_STATE_UPDATED_EVENT_TOPIC, itemName); - ItemEventPayloadBean bean = new ItemEventPayloadBean(getStateType(state), state.toFullString()); + ItemStateUpdatedEventPayloadBean bean = new ItemStateUpdatedEventPayloadBean(getStateType(state), + state.toFullString(), lastStateUpdate); String payload = serializePayload(bean); - return new ItemStateUpdatedEvent(topic, payload, itemName, state, source); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, lastStateUpdate, source); } public static ItemTimeSeriesEvent createTimeSeriesEvent(String itemName, TimeSeries timeSeries, @@ -365,17 +378,19 @@ public static ItemTimeSeriesUpdatedEvent createTimeSeriesUpdatedEvent(String ite * @param groupName the name of the group to report the state update for * @param member the name of the item that updated the group state * @param state the new state + * @param lastStateUpdate the time of the last state update * @param source the name of the source identifying the sender (can be null) * @return the created group item state update event * @throws IllegalArgumentException if groupName or state is null */ public static GroupStateUpdatedEvent createGroupStateUpdatedEvent(String groupName, String member, State state, - @Nullable String source) { + @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) { assertValidArguments(groupName, member, state, "state"); String topic = buildGroupTopic(GROUP_STATE_EVENT_TOPIC, groupName, member); - ItemEventPayloadBean bean = new ItemEventPayloadBean(getStateType(state), state.toFullString()); + ItemStateUpdatedEventPayloadBean bean = new ItemStateUpdatedEventPayloadBean(getStateType(state), + state.toFullString(), lastStateUpdate); String payload = serializePayload(bean); - return new GroupStateUpdatedEvent(topic, payload, groupName, member, state, source); + return new GroupStateUpdatedEvent(topic, payload, groupName, member, state, lastStateUpdate, source); } /** @@ -403,16 +418,20 @@ public static ItemStatePredictedEvent createStatePredictedEvent(String itemName, * @param itemName the name of the item to send the state changed event for * @param newState the new state to send * @param oldState the old state of the item + * @param lastStateChange the time of the last state change * @return the created item state changed event * @throws IllegalArgumentException if itemName or state is null */ - public static ItemStateChangedEvent createStateChangedEvent(String itemName, State newState, State oldState) { + public static ItemStateChangedEvent createStateChangedEvent(String itemName, State newState, State oldState, + @Nullable ZonedDateTime lastStateUpdate, @Nullable ZonedDateTime lastStateChange) { assertValidArguments(itemName, newState, "state"); String topic = buildTopic(ITEM_STATE_CHANGED_EVENT_TOPIC, itemName); ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState), - newState.toFullString(), getStateType(oldState), oldState.toFullString()); + newState.toFullString(), getStateType(oldState), oldState.toFullString(), lastStateUpdate, + lastStateChange); String payload = serializePayload(bean); - return new ItemStateChangedEvent(topic, payload, itemName, newState, oldState); + return new ItemStateChangedEvent(topic, payload, itemName, newState, oldState, lastStateUpdate, + lastStateChange); } /** @@ -422,17 +441,22 @@ public static ItemStateChangedEvent createStateChangedEvent(String itemName, Sta * @param memberName the name of the member causing the group item state change * @param newState the new state to send * @param oldState the old state of the group item + * @param lastStateUpdate the time of the last state update + * @param lastStateChange the time of the last state change * @return the created group item state changed event * @throws IllegalArgumentException if itemName or state is null */ public static GroupItemStateChangedEvent createGroupStateChangedEvent(String itemName, String memberName, - State newState, State oldState) { + State newState, State oldState, @Nullable ZonedDateTime lastStateUpdate, + @Nullable ZonedDateTime lastStateChange) { assertValidArguments(itemName, memberName, newState, "state"); String topic = buildGroupTopic(GROUPITEM_STATE_CHANGED_EVENT_TOPIC, itemName, memberName); ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState), - newState.toFullString(), getStateType(oldState), oldState.toFullString()); + newState.toFullString(), getStateType(oldState), oldState.toFullString(), lastStateUpdate, + lastStateChange); String payload = serializePayload(bean); - return new GroupItemStateChangedEvent(topic, payload, itemName, memberName, newState, oldState); + return new GroupItemStateChangedEvent(topic, payload, itemName, memberName, newState, oldState, lastStateUpdate, + lastStateChange); } /** @@ -551,6 +575,40 @@ public String getValue() { } } + /** + * This is a java bean that is used to serialize/deserialize item state updated event payload. + */ + private static class ItemStateUpdatedEventPayloadBean { + private @NonNullByDefault({}) String type; + private @NonNullByDefault({}) String value; + private @Nullable ZonedDateTime lastUpdate; + + /** + * Default constructor for deserialization e.g. by Gson. + */ + @SuppressWarnings("unused") + protected ItemStateUpdatedEventPayloadBean() { + } + + public ItemStateUpdatedEventPayloadBean(String type, String value, @Nullable ZonedDateTime lastUpdate) { + this.type = type; + this.value = value; + this.lastUpdate = lastUpdate; + } + + public String getType() { + return type; + } + + public String getValue() { + return value; + } + + public @Nullable ZonedDateTime getLastUpdate() { + return lastUpdate; + } + } + /** * This is a java bean that is used to serialize/deserialize item state changed event payload. */ @@ -593,6 +651,8 @@ private static class ItemStateChangedEventPayloadBean { private @NonNullByDefault({}) String value; private @NonNullByDefault({}) String oldType; private @NonNullByDefault({}) String oldValue; + private @Nullable ZonedDateTime lastStateUpdate; + private @Nullable ZonedDateTime lastStateChange; /** * Default constructor for deserialization e.g. by Gson. @@ -601,11 +661,14 @@ private static class ItemStateChangedEventPayloadBean { protected ItemStateChangedEventPayloadBean() { } - public ItemStateChangedEventPayloadBean(String type, String value, String oldType, String oldValue) { + public ItemStateChangedEventPayloadBean(String type, String value, String oldType, String oldValue, + @Nullable ZonedDateTime lastStateUpdate, @Nullable ZonedDateTime lastStateChange) { this.type = type; this.value = value; this.oldType = oldType; this.oldValue = oldValue; + this.lastStateUpdate = lastStateUpdate; + this.lastStateChange = lastStateChange; } public String getType() { @@ -623,6 +686,14 @@ public String getOldType() { public String getOldValue() { return oldValue; } + + public @Nullable ZonedDateTime getLastStateUpdate() { + return lastStateUpdate; + } + + public @Nullable ZonedDateTime getLastStateChange() { + return lastStateChange; + } } private static class ItemTimeSeriesEventPayloadBean { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateChangedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateChangedEvent.java index a2ee7f39c60..8015960cc55 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateChangedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateChangedEvent.java @@ -12,7 +12,10 @@ */ package org.openhab.core.items.events; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; /** @@ -34,6 +37,10 @@ public class ItemStateChangedEvent extends ItemEvent { protected final State oldItemState; + protected final @Nullable ZonedDateTime lastStateUpdate; + + protected final @Nullable ZonedDateTime lastStateChange; + /** * Constructs a new item state changed event. * @@ -42,12 +49,16 @@ public class ItemStateChangedEvent extends ItemEvent { * @param itemName the item name * @param newItemState the new item state * @param oldItemState the old item state + * @param lastStateUpdate the last state update + * @param lastStateChange the last state change */ protected ItemStateChangedEvent(String topic, String payload, String itemName, State newItemState, - State oldItemState) { + State oldItemState, @Nullable ZonedDateTime lastStateUpdate, @Nullable ZonedDateTime lastStateChange) { super(topic, payload, itemName, null); this.itemState = newItemState; this.oldItemState = oldItemState; + this.lastStateUpdate = lastStateUpdate; + this.lastStateChange = lastStateChange; } @Override @@ -73,6 +84,24 @@ public State getOldItemState() { return oldItemState; } + /** + * Gets the timestamp of the previous state update that occurred prior to this event. + * + * @return the last state update + */ + public @Nullable ZonedDateTime getLastStateUpdate() { + return lastStateUpdate; + } + + /** + * Gets the timestamp of the previous state change that occurred prior to this event. + * + * @return the last state change + */ + public @Nullable ZonedDateTime getLastStateChange() { + return lastStateChange; + } + @Override public String toString() { return String.format("Item '%s' changed from %s to %s", itemName, oldItemState, itemState); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java index 31ddb0330ff..27cf55d0d97 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java @@ -12,6 +12,8 @@ */ package org.openhab.core.items.events; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; @@ -31,6 +33,7 @@ public class ItemStateUpdatedEvent extends ItemEvent { public static final String TYPE = ItemStateUpdatedEvent.class.getSimpleName(); protected final State itemState; + protected final @Nullable ZonedDateTime lastStateUpdate; /** * Constructs a new item state event. @@ -39,12 +42,14 @@ public class ItemStateUpdatedEvent extends ItemEvent { * @param payload the payload * @param itemName the item name * @param itemState the item state + * @param lastStateUpdate the last state update * @param source the source, can be null */ protected ItemStateUpdatedEvent(String topic, String payload, String itemName, State itemState, - @Nullable String source) { + @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) { super(topic, payload, itemName, source); this.itemState = itemState; + this.lastStateUpdate = lastStateUpdate; } @Override @@ -61,6 +66,15 @@ public State getItemState() { return itemState; } + /** + * Gets the timestamp of the previous state update that occurred prior to this event. + * + * @return the last state update + */ + public @Nullable ZonedDateTime getLastStateUpdate() { + return lastStateUpdate; + } + @Override public String toString() { return String.format("Item '%s' updated to %s", itemName, itemState); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java index 771b24bf328..06d24ec6358 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java @@ -142,7 +142,7 @@ void testIgnoreStateUpdateExtendsExpiryOnStateChange() throws InterruptedExcepti Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1)); expireManager.receive(event); Thread.sleep(1500L); - event = ItemEventFactory.createStateChangedEvent(ITEMNAME, new DecimalType(2), new DecimalType(1)); + event = ItemEventFactory.createStateChangedEvent(ITEMNAME, new DecimalType(2), new DecimalType(1), null, null); expireManager.receive(event); Thread.sleep(1500L); verify(eventPublisherMock, never()).post(any()); @@ -194,7 +194,7 @@ void testIgnoreCommandsExtendsExpiryOnStateChange() throws InterruptedException, Event event = ItemEventFactory.createStateEvent(ITEMNAME, new DecimalType(1)); expireManager.receive(event); Thread.sleep(1500L); - event = ItemEventFactory.createStateChangedEvent(ITEMNAME, new DecimalType(2), new DecimalType(1)); + event = ItemEventFactory.createStateChangedEvent(ITEMNAME, new DecimalType(2), new DecimalType(1), null, null); expireManager.receive(event); Thread.sleep(1500L); verify(eventPublisherMock, never()).post(any()); 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 bd4643a1147..f13dda70757 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 @@ -56,6 +56,8 @@ public class GenericItemTest { @Test public void testItemPostsEventsCorrectly() { + ZonedDateTime lastStateUpdate; + ZonedDateTime lastStateChange; EventPublisher publisher = mock(EventPublisher.class); TestItem item = new TestItem("member1"); @@ -78,6 +80,7 @@ public void testItemPostsEventsCorrectly() { assertEquals(item.getName(), updated.getItemName()); assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); assertEquals(item.getState(), updated.getItemState()); + assertEquals(null, updated.getLastStateUpdate()); // this is the first update, so there is no previous update assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); // second event should be changed event @@ -87,12 +90,16 @@ public void testItemPostsEventsCorrectly() { assertEquals("openhab/items/member1/statechanged", change.getTopic()); assertEquals(oldState, change.getOldItemState()); assertEquals(item.getState(), change.getItemState()); + assertEquals(null, change.getLastStateChange()); // this is the first change, so there is no previous change assertEquals(ItemStateChangedEvent.TYPE, change.getType()); // reset invocations and captor clearInvocations(publisher); captor = ArgumentCaptor.forClass(ItemEvent.class); + lastStateChange = item.getLastStateChange(); + lastStateUpdate = item.getLastStateUpdate(); + // State doesn't change -> only update event is fired item.setState(item.getState()); verify(publisher).post(captor.capture()); @@ -106,7 +113,25 @@ public void testItemPostsEventsCorrectly() { assertEquals(item.getName(), updated.getItemName()); assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); assertEquals(item.getState(), updated.getItemState()); + assertEquals(lastStateUpdate, updated.getLastStateUpdate()); assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); + + // State changes -> the ItemStateChangedEvent should include the lastStateChange + clearInvocations(publisher); + captor = ArgumentCaptor.forClass(ItemEvent.class); + + lastStateUpdate = item.getLastStateUpdate(); + + // New State + item.setState(new RawType(new byte[1], RawType.DEFAULT_MIME_TYPE)); + verify(publisher, times(2)).post(captor.capture()); + + events = captor.getAllValues(); + assertEquals(2, events.size()); + assertInstanceOf(ItemStateChangedEvent.class, events.get(1)); + change = (ItemStateChangedEvent) events.get(1); + assertEquals(lastStateUpdate, change.getLastStateUpdate()); + assertEquals(lastStateChange, change.getLastStateChange()); } @Test diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/events/ItemEventFactoryTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/events/ItemEventFactoryTest.java index c9cac14f5f2..8a961cf7d6e 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/events/ItemEventFactoryTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/events/ItemEventFactoryTest.java @@ -16,6 +16,8 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.*; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.core.events.Event; @@ -224,8 +226,10 @@ public void testCreateAddedEvent() { @Test public void testCreateGroupStateChangedEventRawType() throws Exception { + ZonedDateTime lastStateUpdate = ZonedDateTime.now(); + ZonedDateTime lastStateChange = ZonedDateTime.now().minusMinutes(1); GroupItemStateChangedEvent giEventSource = ItemEventFactory.createGroupStateChangedEvent(GROUP_NAME, ITEM_NAME, - NEW_RAW_ITEM_STATE, RAW_ITEM_STATE); + NEW_RAW_ITEM_STATE, RAW_ITEM_STATE, lastStateUpdate, lastStateChange); Event giEventParsed = factory.createEvent(giEventSource.getType(), giEventSource.getTopic(), giEventSource.getPayload(), giEventSource.getSource()); @@ -241,5 +245,7 @@ public void testCreateGroupStateChangedEventRawType() throws Exception { assertNull(groupItemStateChangedEvent.getSource()); assertEquals(NEW_RAW_ITEM_STATE, groupItemStateChangedEvent.getItemState()); assertEquals(RAW_ITEM_STATE, groupItemStateChangedEvent.getOldItemState()); + assertEquals(lastStateUpdate, groupItemStateChangedEvent.getLastStateUpdate()); + assertEquals(lastStateChange, groupItemStateChangedEvent.getLastStateChange()); } } diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java index 66e14a00659..8eecc29042f 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java @@ -191,7 +191,7 @@ public void receive(Event event) { logger.info("Rule is enabled and idle"); logger.info("Send and wait for item state is ON"); - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); waitForAssert(() -> { assertThat(itemEvent, is(notNullValue())); @@ -207,7 +207,7 @@ public void receive(Event event) { // prepare the execution itemEvent = null; - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing assertThat(itemEvent, is(nullValue())); } diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/IntervalConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/IntervalConditionHandlerTest.java index a27bc868092..2f5423ebeff 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/IntervalConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/IntervalConditionHandlerTest.java @@ -168,7 +168,7 @@ public void receive(Event event) { logger.info("Rule is enabled and idle"); logger.info("Send and wait for item state is ON"); - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); // the first event is always processed waitForAssert(() -> { @@ -182,7 +182,7 @@ public void receive(Event event) { // Send a second event to check if the condition is still satisfied itemEvent = null; // reset it - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); waitForAssert(() -> { assertThat(itemEvent, is(notNullValue())); @@ -198,7 +198,7 @@ public void receive(Event event) { // prepare the execution itemEvent = null; - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); // the first event is always allowed waitForAssert(() -> { @@ -209,7 +209,7 @@ public void receive(Event event) { // the second event is not allowed itemEvent = null; - eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null)); Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing assertThat(itemEvent, is(nullValue())); } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java index cc0d9177419..c6a02ffcad3 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java @@ -360,7 +360,7 @@ public void testItemCommandEventNotToSource() { @Test public void testItemStateEventSingleLink() { - manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON, null)); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -371,7 +371,7 @@ public void testItemStateEventSingleLink() { @Test public void testItemStateEventMultiLink() { - manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, null)); waitForAssert(() -> { verify(stateProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -382,8 +382,8 @@ public void testItemStateEventMultiLink() { @Test public void testItemStateEventNotToSource() { - manager.receive( - ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, null, + STATE_CHANNEL_UID_2.getAsString())); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON));