Add `lastStateUpdate`, `lastStateChange` to `ItemStateUpdatedEvent`/`ItemStateChangedEvent` (#4606)

* Add `lastStateUpdate`, `lastStateChange` to `ItemStateUpdatedEvent`/`ItemStateChangedEvent`

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
pull/4693/head
jimtng 2025-04-03 05:59:16 +10:00 committed by GitHub
parent 40d9a67e53
commit 9cd72164f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 338 additions and 74 deletions

View File

@ -122,12 +122,13 @@ public class GroupStateTriggerHandler extends BaseTriggerModuleHandler implement
if (item != null && item.getGroupNames().contains(groupName)) { if (item != null && item.getGroupNames().contains(groupName)) {
State state = isEvent.getItemState(); State state = isEvent.getItemState();
if ((this.state == null || state.toFullString().equals(this.state))) { if ((this.state == null || state.toFullString().equals(this.state))) {
Map<String, Object> values = new HashMap<>(); Map<String, @Nullable Object> values = new HashMap<>();
if (group != null) { if (group != null) {
values.put("triggeringGroup", group); values.put("triggeringGroup", group);
} }
values.put("triggeringItem", item); values.put("triggeringItem", item);
values.put("state", state); values.put("state", state);
values.put("lastStateUpdate", isEvent.getLastStateUpdate());
values.put("event", event); values.put("event", event);
cb.triggered(this.module, values); cb.triggered(this.module, values);
} }
@ -142,13 +143,15 @@ public class GroupStateTriggerHandler extends BaseTriggerModuleHandler implement
State oldState = iscEvent.getOldItemState(); State oldState = iscEvent.getOldItemState();
if (stateMatches(this.state, state) && stateMatches(this.previousState, oldState)) { if (stateMatches(this.state, state) && stateMatches(this.previousState, oldState)) {
Map<String, Object> values = new HashMap<>(); Map<String, @Nullable Object> values = new HashMap<>();
if (group != null) { if (group != null) {
values.put("triggeringGroup", group); values.put("triggeringGroup", group);
} }
values.put("triggeringItem", item); values.put("triggeringItem", item);
values.put("oldState", oldState); values.put("oldState", oldState);
values.put("newState", state); values.put("newState", state);
values.put("lastStateUpdate", iscEvent.getLastStateUpdate());
values.put("lastStateChange", iscEvent.getLastStateChange());
values.put("event", event); values.put("event", event);
cb.triggered(this.module, values); cb.triggered(this.module, values);
} }

View File

@ -132,13 +132,14 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements
if (callback != null) { if (callback != null) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload()); event.getTopic(), event.getType(), event.getPayload());
Map<String, Object> values = new HashMap<>(); Map<String, @Nullable Object> values = new HashMap<>();
if (event instanceof ItemStateUpdatedEvent updatedEvent if (event instanceof ItemStateUpdatedEvent updatedEvent
&& UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
String state = this.state; String state = this.state;
State itemState = updatedEvent.getItemState(); State itemState = updatedEvent.getItemState();
if ((state == null || state.equals(itemState.toFullString()))) { if ((state == null || state.equals(itemState.toFullString()))) {
values.put("state", itemState); values.put("state", itemState);
values.put("lastStateUpdate", updatedEvent.getLastStateUpdate());
} }
} else if (event instanceof ItemStateChangedEvent changedEvent } else if (event instanceof ItemStateChangedEvent changedEvent
&& CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) { && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
@ -148,6 +149,8 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements
if (stateMatches(this.state, itemState) && stateMatches(this.previousState, oldItemState)) { if (stateMatches(this.state, itemState) && stateMatches(this.previousState, oldItemState)) {
values.put("oldState", oldItemState); values.put("oldState", oldItemState);
values.put("newState", itemState); values.put("newState", itemState);
values.put("lastStateUpdate", changedEvent.getLastStateUpdate());
values.put("lastStateChange", changedEvent.getLastStateChange());
} }
} }
if (!values.isEmpty()) { if (!values.isEmpty()) {

View File

@ -125,6 +125,12 @@
"state" "state"
] ]
}, },
{
"name": "lastStateUpdate",
"type": "java.time.ZonedDateTime",
"description": "the time of the previous state update",
"label": "Last State Update"
},
{ {
"name": "event", "name": "event",
"type": "org.openhab.core.events.Event", "type": "org.openhab.core.events.Event",
@ -220,8 +226,8 @@
{ {
"name": "newState", "name": "newState",
"type": "state", "type": "state",
"description": "the new item state",
"label": "New State", "label": "New State",
"description": "the new item state",
"tags": [ "tags": [
"state" "state"
] ]
@ -229,8 +235,20 @@
{ {
"name": "oldState", "name": "oldState",
"type": "state", "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", "name": "event",
@ -402,6 +420,12 @@
"state" "state"
] ]
}, },
{
"name": "lastStateUpdate",
"type": "java.time.ZonedDateTime",
"description": "the time of the previous state update",
"label": "Last State Update"
},
{ {
"name": "event", "name": "event",
"type": "org.openhab.core.events.Event", "type": "org.openhab.core.events.Event",
@ -518,6 +542,18 @@
"description": "the old item state", "description": "the old item state",
"label": "Old 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", "name": "event",
"type": "org.openhab.core.events.Event", "type": "org.openhab.core.events.Event",

View File

@ -258,7 +258,8 @@ public class EventWebSocketTest {
eventWebSocket.processEvent(event); eventWebSocket.processEvent(event);
verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(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); eventWebSocket.processEvent(event);
verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event)));
@ -285,7 +286,8 @@ public class EventWebSocketTest {
verify(remoteEndpoint, times(0)).sendString(any()); verify(remoteEndpoint, times(0)).sendString(any());
// not excluded topics are sent // 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); eventWebSocket.processEvent(event);
verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event)));
@ -309,7 +311,8 @@ public class EventWebSocketTest {
clearInvocations(remoteEndpoint); clearInvocations(remoteEndpoint);
// included topics are sent // 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); eventWebSocket.processEvent(event);
verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event))); verify(remoteEndpoint).sendString(gson.toJson(new EventDTO(event)));

View File

@ -13,6 +13,7 @@
package org.openhab.core.model.rule.jvmmodel package org.openhab.core.model.rule.jvmmodel
import com.google.inject.Inject import com.google.inject.Inject
import java.time.ZonedDateTime
import java.util.Set import java.util.Set
import org.openhab.core.items.Item import org.openhab.core.items.Item
import org.openhab.core.items.ItemRegistry import org.openhab.core.items.ItemRegistry
@ -145,10 +146,22 @@ class RulesJvmModelInferrer extends ScriptJvmModelInferrer {
val commandTypeRef = typeRef(Command) val commandTypeRef = typeRef(Command)
parameters += rule.toParameter(VAR_RECEIVED_COMMAND, commandTypeRef) parameters += rule.toParameter(VAR_RECEIVED_COMMAND, commandTypeRef)
} }
if ((containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) && !containsParam(parameters, VAR_NEW_STATE)) {
val stateTypeRef = typeRef(State)
parameters += rule.toParameter(VAR_NEW_STATE, stateTypeRef)
}
if (containsStateChangeTrigger(rule) && !containsParam(parameters, VAR_PREVIOUS_STATE)) { if (containsStateChangeTrigger(rule) && !containsParam(parameters, VAR_PREVIOUS_STATE)) {
val stateTypeRef = typeRef(State) val stateTypeRef = typeRef(State)
parameters += rule.toParameter(VAR_PREVIOUS_STATE, stateTypeRef) parameters += rule.toParameter(VAR_PREVIOUS_STATE, stateTypeRef)
} }
if (containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) {
val lastStateUpdateTypeRef = typeRef(ZonedDateTime)
parameters += rule.toParameter(VAR_LAST_STATE_UPDATE, lastStateUpdateTypeRef)
}
if (containsStateChangeTrigger(rule)) {
val lastStateChangeTypeRef = typeRef(ZonedDateTime)
parameters += rule.toParameter(VAR_LAST_STATE_CHANGE, lastStateChangeTypeRef)
}
if (containsEventTrigger(rule)) { if (containsEventTrigger(rule)) {
val eventTypeRef = typeRef(String) val eventTypeRef = typeRef(String)
parameters += rule.toParameter(VAR_RECEIVED_EVENT, eventTypeRef) parameters += rule.toParameter(VAR_RECEIVED_EVENT, eventTypeRef)
@ -163,10 +176,6 @@ class RulesJvmModelInferrer extends ScriptJvmModelInferrer {
val newStatusRef = typeRef(String) val newStatusRef = typeRef(String)
parameters += rule.toParameter(VAR_NEW_STATUS, newStatusRef) parameters += rule.toParameter(VAR_NEW_STATUS, newStatusRef)
} }
if ((containsStateChangeTrigger(rule) || containsStateUpdateTrigger(rule)) && !containsParam(parameters, VAR_NEW_STATE)) {
val stateTypeRef = typeRef(State)
parameters += rule.toParameter(VAR_NEW_STATE, stateTypeRef)
}
body = rule.script body = rule.script
] ]

View File

@ -61,11 +61,16 @@ public class DSLScriptEngine implements javax.script.ScriptEngine {
public static final String MIMETYPE_OPENHAB_DSL_RULE = "application/vnd.openhab.dsl.rule"; public static final String MIMETYPE_OPENHAB_DSL_RULE = "application/vnd.openhab.dsl.rule";
private static final Map<String, String> IMPLICIT_VARS = Map.of("command", private static final Map<String, String> IMPLICIT_VARS = Map.of( //
ScriptJvmModelInferrer.VAR_RECEIVED_COMMAND, "state", ScriptJvmModelInferrer.VAR_NEW_STATE, "newState", "command", ScriptJvmModelInferrer.VAR_RECEIVED_COMMAND, //
ScriptJvmModelInferrer.VAR_NEW_STATE, "oldState", ScriptJvmModelInferrer.VAR_PREVIOUS_STATE, "state", ScriptJvmModelInferrer.VAR_NEW_STATE, //
"triggeringItem", ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM, "triggeringGroup", "newState", ScriptJvmModelInferrer.VAR_NEW_STATE, //
ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP, "input", ScriptJvmModelInferrer.VAR_INPUT); "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); private final Logger logger = LoggerFactory.getLogger(DSLScriptEngine.class);

View File

@ -13,6 +13,7 @@
package org.openhab.core.model.script.jvmmodel package org.openhab.core.model.script.jvmmodel
import com.google.inject.Inject import com.google.inject.Inject
import java.time.ZonedDateTime
import java.util.Set import java.util.Set
import org.openhab.core.items.ItemRegistry import org.openhab.core.items.ItemRegistry
import org.openhab.core.model.script.scoping.StateAndCommandProvider 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 */ /** 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"; 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 */ /** Variable name for the received command in a "command triggered" rule */
public static final String VAR_RECEIVED_COMMAND = "receivedCommand"; public static final String VAR_RECEIVED_COMMAND = "receivedCommand";
@ -160,6 +167,10 @@ class ScriptJvmModelInferrer extends AbstractModelInferrer {
parameters += script.toParameter(VAR_NEW_STATUS, newThingStatusRef) parameters += script.toParameter(VAR_NEW_STATUS, newThingStatusRef)
val stateTypeRef2 = typeRef(State) val stateTypeRef2 = typeRef(State)
parameters += script.toParameter(VAR_NEW_STATE, stateTypeRef2) parameters += script.toParameter(VAR_NEW_STATE, stateTypeRef2)
val lastStateUpdateTypeRef = typeRef(ZonedDateTime)
parameters += script.toParameter(VAR_LAST_STATE_UPDATE, lastStateUpdateTypeRef)
val lastStateChangeTypeRef = typeRef(ZonedDateTime)
parameters += script.toParameter(VAR_LAST_STATE_CHANGE, lastStateChangeTypeRef)
val privateCacheTypeRef = typeRef(ValueCache) val privateCacheTypeRef = typeRef(ValueCache)
parameters += script.toParameter(VAR_PRIVATE_CACHE, privateCacheTypeRef) parameters += script.toParameter(VAR_PRIVATE_CACHE, privateCacheTypeRef)
val sharedCacheTypeRef = typeRef(ValueCache) val sharedCacheTypeRef = typeRef(ValueCache)

View File

@ -12,12 +12,20 @@
*/ */
package org.openhab.core.events; package org.openhab.core.events;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Set; import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson; 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 * 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<String> supportedEventTypes; private final Set<String> 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. * Must be called in subclass constructor to define the supported event types.
@ -120,4 +129,25 @@ public abstract class AbstractEventFactory implements EventFactory {
throw new IllegalArgumentException("The argument '" + argumentName + "' must not be null or empty."); throw new IllegalArgumentException("The argument '" + argumentName + "' must not be null or empty.");
} }
} }
public static class ZonedDateTimeAdapter extends TypeAdapter<ZonedDateTime> {
@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);
}
}
} }

View File

@ -242,14 +242,15 @@ public abstract class GenericItem implements ActiveItem {
public void setState(State state, @Nullable State lastState, @Nullable ZonedDateTime lastStateUpdate, public void setState(State state, @Nullable State lastState, @Nullable ZonedDateTime lastStateUpdate,
@Nullable ZonedDateTime lastStateChange) { @Nullable ZonedDateTime lastStateChange) {
State oldState = this.state; State oldState = this.state;
ZonedDateTime oldStateUpdate = this.lastStateUpdate;
this.state = state; this.state = state;
this.lastState = lastState != null ? lastState : this.lastState; this.lastState = lastState != null ? lastState : this.lastState;
this.lastStateUpdate = lastStateUpdate != null ? lastStateUpdate : this.lastStateUpdate; this.lastStateUpdate = lastStateUpdate != null ? lastStateUpdate : this.lastStateUpdate;
this.lastStateChange = lastStateChange != null ? lastStateChange : this.lastStateChange; this.lastStateChange = lastStateChange != null ? lastStateChange : this.lastStateChange;
notifyListeners(oldState, state); notifyListeners(oldState, state);
sendStateUpdatedEvent(state); sendStateUpdatedEvent(state, lastStateUpdate);
if (!oldState.equals(state)) { if (!oldState.equals(state)) {
sendStateChangedEvent(state, oldState); sendStateChangedEvent(state, oldState, lastStateUpdate, lastStateChange);
} }
} }
@ -270,9 +271,9 @@ public abstract class GenericItem implements ActiveItem {
lastState = oldState; // update before we notify listeners lastState = oldState; // update before we notify listeners
} }
notifyListeners(oldState, state); notifyListeners(oldState, state);
sendStateUpdatedEvent(state); sendStateUpdatedEvent(state, lastStateUpdate);
if (stateChanged) { if (stateChanged) {
sendStateChangedEvent(state, oldState); sendStateChangedEvent(state, oldState, lastStateUpdate, lastStateChange);
lastStateChange = now; // update after we've notified listeners lastStateChange = now; // update after we've notified listeners
} }
lastStateUpdate = now; lastStateUpdate = now;
@ -322,17 +323,19 @@ public abstract class GenericItem implements ActiveItem {
} }
} }
private void sendStateUpdatedEvent(State newState) { private void sendStateUpdatedEvent(State newState, @Nullable ZonedDateTime lastStateUpdate) {
EventPublisher eventPublisher1 = this.eventPublisher; EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) { 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; EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) { if (eventPublisher1 != null) {
eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState,
lastStateUpdate, lastStateChange));
} }
} }

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.core.items; package org.openhab.core.items;
import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -365,19 +366,22 @@ public class GroupItem extends GenericItem implements StateChangeListener, Metad
State oldState = this.state; State oldState = this.state;
State newState = oldState; State newState = oldState;
ItemStateConverter itemStateConverter = this.itemStateConverter; ItemStateConverter itemStateConverter = this.itemStateConverter;
ZonedDateTime lastStateUpdate = this.lastStateUpdate;
ZonedDateTime lastStateChange = this.lastStateChange;
if (function instanceof GroupFunction groupFunction && baseItem != null && itemStateConverter != null) { if (function instanceof GroupFunction groupFunction && baseItem != null && itemStateConverter != null) {
State calculatedState = groupFunction.calculate(getStateMembers(getMembers())); State calculatedState = groupFunction.calculate(getStateMembers(getMembers()));
newState = itemStateConverter.convertToAcceptedState(calculatedState, baseItem); newState = itemStateConverter.convertToAcceptedState(calculatedState, baseItem);
setState(newState); setState(newState);
sendGroupStateUpdatedEvent(item.getName(), newState); sendGroupStateUpdatedEvent(item.getName(), newState, lastStateUpdate);
} }
if (!oldState.equals(newState)) { if (!oldState.equals(newState)) {
sendGroupStateChangedEvent(item.getName(), newState, oldState); sendGroupStateChangedEvent(item.getName(), newState, oldState, lastStateUpdate, lastStateChange);
} }
} }
@Override @Override
public void setState(State state) { public void setState(State state) {
ZonedDateTime now = ZonedDateTime.now();
State oldState = this.state; State oldState = this.state;
Item baseItem = this.baseItem; Item baseItem = this.baseItem;
if (baseItem instanceof GenericItem item) { if (baseItem instanceof GenericItem item) {
@ -387,6 +391,10 @@ public class GroupItem extends GenericItem implements StateChangeListener, Metad
this.state = state; this.state = state;
} }
notifyListeners(oldState, state); notifyListeners(oldState, state);
if (!oldState.equals(state)) {
lastStateChange = now;
}
lastStateUpdate = now;
} }
@Override @Override
@ -405,18 +413,20 @@ public class GroupItem extends GenericItem implements StateChangeListener, Metad
} }
} }
private void sendGroupStateUpdatedEvent(String memberName, State state) { private void sendGroupStateUpdatedEvent(String memberName, State state, @Nullable ZonedDateTime lastStateUpdate) {
EventPublisher eventPublisher1 = this.eventPublisher; EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) { 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; EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) { if (eventPublisher1 != null) {
eventPublisher1 eventPublisher1.post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState,
.post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState, oldState)); oldState, lastStateUpdate, lastStateChange));
} }
} }

View File

@ -12,7 +12,10 @@
*/ */
package org.openhab.core.items.events; package org.openhab.core.items.events;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State; import org.openhab.core.types.State;
/** /**
@ -34,8 +37,9 @@ public class GroupItemStateChangedEvent extends ItemStateChangedEvent {
private final String memberName; private final String memberName;
protected GroupItemStateChangedEvent(String topic, String payload, String itemName, String memberName, protected GroupItemStateChangedEvent(String topic, String payload, String itemName, String memberName,
State newItemState, State oldItemState) { State newItemState, State oldItemState, @Nullable ZonedDateTime lastStateUpdate,
super(topic, payload, itemName, newItemState, oldItemState); @Nullable ZonedDateTime lastStateChange) {
super(topic, payload, itemName, newItemState, oldItemState, lastStateUpdate, lastStateChange);
this.memberName = memberName; this.memberName = memberName;
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.core.items.events; package org.openhab.core.items.events;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State; import org.openhab.core.types.State;
@ -34,8 +36,8 @@ public class GroupStateUpdatedEvent extends ItemStateUpdatedEvent {
private final String memberName; private final String memberName;
protected GroupStateUpdatedEvent(String topic, String payload, String itemName, String memberName, protected GroupStateUpdatedEvent(String topic, String payload, String itemName, String memberName,
State newItemState, @Nullable String source) { State newItemState, @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) {
super(topic, payload, itemName, newItemState, source); super(topic, payload, itemName, newItemState, lastStateUpdate, source);
this.memberName = memberName; this.memberName = memberName;
} }

View File

@ -15,6 +15,7 @@ package org.openhab.core.items.events;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Instant; import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -114,9 +115,10 @@ public class ItemEventFactory extends AbstractEventFactory {
private Event createGroupStateUpdatedEvent(String topic, String payload) { private Event createGroupStateUpdatedEvent(String topic, String payload) {
String itemName = getItemName(topic); String itemName = getItemName(topic);
String memberName = getMemberName(topic); String memberName = getMemberName(topic);
ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); ItemStateUpdatedEventPayloadBean bean = deserializePayload(payload, ItemStateUpdatedEventPayloadBean.class);
State state = getState(bean.getType(), bean.getValue()); 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) { private Event createGroupStateChangedEvent(String topic, String payload) {
@ -125,7 +127,10 @@ public class ItemEventFactory extends AbstractEventFactory {
ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class);
State state = getState(bean.getType(), bean.getValue()); State state = getState(bean.getType(), bean.getValue());
State oldState = getState(bean.getOldType(), bean.getOldValue()); 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) { private Event createCommandEvent(String topic, String payload, @Nullable String source) {
@ -151,9 +156,10 @@ public class ItemEventFactory extends AbstractEventFactory {
private Event createStateUpdatedEvent(String topic, String payload) { private Event createStateUpdatedEvent(String topic, String payload) {
String itemName = getItemName(topic); String itemName = getItemName(topic);
ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); ItemStateUpdatedEventPayloadBean bean = deserializePayload(payload, ItemStateUpdatedEventPayloadBean.class);
State state = getState(bean.getType(), bean.getValue()); 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) { private Event createStateChangedEvent(String topic, String payload) {
@ -161,7 +167,9 @@ public class ItemEventFactory extends AbstractEventFactory {
ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class);
State state = getState(bean.getType(), bean.getValue()); State state = getState(bean.getType(), bean.getValue());
State oldState = getState(bean.getOldType(), bean.getOldValue()); 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) { private Event createTimeSeriesEvent(String topic, String payload) {
@ -319,11 +327,13 @@ public class ItemEventFactory extends AbstractEventFactory {
* *
* @param itemName the name of the item to report the state update for * @param itemName the name of the item to report the state update for
* @param state the new state * @param state the new state
* @param lastStateUpdate the time of the last state update
* @return the created item state update event * @return the created item state update event
* @throws IllegalArgumentException if itemName or state is null * @throws IllegalArgumentException if itemName or state is null
*/ */
public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state) { public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state,
return createStateUpdatedEvent(itemName, state, null); @Nullable ZonedDateTime lastStateUpdate) {
return createStateUpdatedEvent(itemName, state, lastStateUpdate, null);
} }
/** /**
@ -331,16 +341,19 @@ public class ItemEventFactory extends AbstractEventFactory {
* *
* @param itemName the name of the item to report the state update for * @param itemName the name of the item to report the state update for
* @param state the new 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) * @param source the name of the source identifying the sender (can be null)
* @return the created item state update event * @return the created item state update event
* @throws IllegalArgumentException if itemName or state is null * @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"); assertValidArguments(itemName, state, "state");
String topic = buildTopic(ITEM_STATE_UPDATED_EVENT_TOPIC, itemName); 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); 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, public static ItemTimeSeriesEvent createTimeSeriesEvent(String itemName, TimeSeries timeSeries,
@ -365,17 +378,19 @@ public class ItemEventFactory extends AbstractEventFactory {
* @param groupName the name of the group to report the state update for * @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 member the name of the item that updated the group state
* @param state the new 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) * @param source the name of the source identifying the sender (can be null)
* @return the created group item state update event * @return the created group item state update event
* @throws IllegalArgumentException if groupName or state is null * @throws IllegalArgumentException if groupName or state is null
*/ */
public static GroupStateUpdatedEvent createGroupStateUpdatedEvent(String groupName, String member, State state, public static GroupStateUpdatedEvent createGroupStateUpdatedEvent(String groupName, String member, State state,
@Nullable String source) { @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) {
assertValidArguments(groupName, member, state, "state"); assertValidArguments(groupName, member, state, "state");
String topic = buildGroupTopic(GROUP_STATE_EVENT_TOPIC, groupName, member); 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); 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 class ItemEventFactory extends AbstractEventFactory {
* @param itemName the name of the item to send the state changed event for * @param itemName the name of the item to send the state changed event for
* @param newState the new state to send * @param newState the new state to send
* @param oldState the old state of the item * @param oldState the old state of the item
* @param lastStateChange the time of the last state change
* @return the created item state changed event * @return the created item state changed event
* @throws IllegalArgumentException if itemName or state is null * @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"); assertValidArguments(itemName, newState, "state");
String topic = buildTopic(ITEM_STATE_CHANGED_EVENT_TOPIC, itemName); String topic = buildTopic(ITEM_STATE_CHANGED_EVENT_TOPIC, itemName);
ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState), ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState),
newState.toFullString(), getStateType(oldState), oldState.toFullString()); newState.toFullString(), getStateType(oldState), oldState.toFullString(), lastStateUpdate,
lastStateChange);
String payload = serializePayload(bean); 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 class ItemEventFactory extends AbstractEventFactory {
* @param memberName the name of the member causing the group item state change * @param memberName the name of the member causing the group item state change
* @param newState the new state to send * @param newState the new state to send
* @param oldState the old state of the group item * @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 * @return the created group item state changed event
* @throws IllegalArgumentException if itemName or state is null * @throws IllegalArgumentException if itemName or state is null
*/ */
public static GroupItemStateChangedEvent createGroupStateChangedEvent(String itemName, String memberName, 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"); assertValidArguments(itemName, memberName, newState, "state");
String topic = buildGroupTopic(GROUPITEM_STATE_CHANGED_EVENT_TOPIC, itemName, memberName); String topic = buildGroupTopic(GROUPITEM_STATE_CHANGED_EVENT_TOPIC, itemName, memberName);
ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState), ItemStateChangedEventPayloadBean bean = new ItemStateChangedEventPayloadBean(getStateType(newState),
newState.toFullString(), getStateType(oldState), oldState.toFullString()); newState.toFullString(), getStateType(oldState), oldState.toFullString(), lastStateUpdate,
lastStateChange);
String payload = serializePayload(bean); 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 class ItemEventFactory extends AbstractEventFactory {
} }
} }
/**
* 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. * This is a java bean that is used to serialize/deserialize item state changed event payload.
*/ */
@ -593,6 +651,8 @@ public class ItemEventFactory extends AbstractEventFactory {
private @NonNullByDefault({}) String value; private @NonNullByDefault({}) String value;
private @NonNullByDefault({}) String oldType; private @NonNullByDefault({}) String oldType;
private @NonNullByDefault({}) String oldValue; private @NonNullByDefault({}) String oldValue;
private @Nullable ZonedDateTime lastStateUpdate;
private @Nullable ZonedDateTime lastStateChange;
/** /**
* Default constructor for deserialization e.g. by Gson. * Default constructor for deserialization e.g. by Gson.
@ -601,11 +661,14 @@ public class ItemEventFactory extends AbstractEventFactory {
protected 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.type = type;
this.value = value; this.value = value;
this.oldType = oldType; this.oldType = oldType;
this.oldValue = oldValue; this.oldValue = oldValue;
this.lastStateUpdate = lastStateUpdate;
this.lastStateChange = lastStateChange;
} }
public String getType() { public String getType() {
@ -623,6 +686,14 @@ public class ItemEventFactory extends AbstractEventFactory {
public String getOldValue() { public String getOldValue() {
return oldValue; return oldValue;
} }
public @Nullable ZonedDateTime getLastStateUpdate() {
return lastStateUpdate;
}
public @Nullable ZonedDateTime getLastStateChange() {
return lastStateChange;
}
} }
private static class ItemTimeSeriesEventPayloadBean { private static class ItemTimeSeriesEventPayloadBean {

View File

@ -12,7 +12,10 @@
*/ */
package org.openhab.core.items.events; package org.openhab.core.items.events;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State; import org.openhab.core.types.State;
/** /**
@ -34,6 +37,10 @@ public class ItemStateChangedEvent extends ItemEvent {
protected final State oldItemState; protected final State oldItemState;
protected final @Nullable ZonedDateTime lastStateUpdate;
protected final @Nullable ZonedDateTime lastStateChange;
/** /**
* Constructs a new item state changed event. * Constructs a new item state changed event.
* *
@ -42,12 +49,16 @@ public class ItemStateChangedEvent extends ItemEvent {
* @param itemName the item name * @param itemName the item name
* @param newItemState the new item state * @param newItemState the new item state
* @param oldItemState the old 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, 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); super(topic, payload, itemName, null);
this.itemState = newItemState; this.itemState = newItemState;
this.oldItemState = oldItemState; this.oldItemState = oldItemState;
this.lastStateUpdate = lastStateUpdate;
this.lastStateChange = lastStateChange;
} }
@Override @Override
@ -73,6 +84,24 @@ public class ItemStateChangedEvent extends ItemEvent {
return oldItemState; 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 @Override
public String toString() { public String toString() {
return String.format("Item '%s' changed from %s to %s", itemName, oldItemState, itemState); return String.format("Item '%s' changed from %s to %s", itemName, oldItemState, itemState);

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.core.items.events; package org.openhab.core.items.events;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State; import org.openhab.core.types.State;
@ -31,6 +33,7 @@ public class ItemStateUpdatedEvent extends ItemEvent {
public static final String TYPE = ItemStateUpdatedEvent.class.getSimpleName(); public static final String TYPE = ItemStateUpdatedEvent.class.getSimpleName();
protected final State itemState; protected final State itemState;
protected final @Nullable ZonedDateTime lastStateUpdate;
/** /**
* Constructs a new item state event. * Constructs a new item state event.
@ -39,12 +42,14 @@ public class ItemStateUpdatedEvent extends ItemEvent {
* @param payload the payload * @param payload the payload
* @param itemName the item name * @param itemName the item name
* @param itemState the item state * @param itemState the item state
* @param lastStateUpdate the last state update
* @param source the source, can be null * @param source the source, can be null
*/ */
protected ItemStateUpdatedEvent(String topic, String payload, String itemName, State itemState, protected ItemStateUpdatedEvent(String topic, String payload, String itemName, State itemState,
@Nullable String source) { @Nullable ZonedDateTime lastStateUpdate, @Nullable String source) {
super(topic, payload, itemName, source); super(topic, payload, itemName, source);
this.itemState = itemState; this.itemState = itemState;
this.lastStateUpdate = lastStateUpdate;
} }
@Override @Override
@ -61,6 +66,15 @@ public class ItemStateUpdatedEvent extends ItemEvent {
return itemState; 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 @Override
public String toString() { public String toString() {
return String.format("Item '%s' updated to %s", itemName, itemState); return String.format("Item '%s' updated to %s", itemName, itemState);

View File

@ -142,7 +142,7 @@ class ExpireManagerTest {
Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1)); Event event = ItemEventFactory.createCommandEvent(ITEMNAME, new DecimalType(1));
expireManager.receive(event); expireManager.receive(event);
Thread.sleep(1500L); 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); expireManager.receive(event);
Thread.sleep(1500L); Thread.sleep(1500L);
verify(eventPublisherMock, never()).post(any()); verify(eventPublisherMock, never()).post(any());
@ -194,7 +194,7 @@ class ExpireManagerTest {
Event event = ItemEventFactory.createStateEvent(ITEMNAME, new DecimalType(1)); Event event = ItemEventFactory.createStateEvent(ITEMNAME, new DecimalType(1));
expireManager.receive(event); expireManager.receive(event);
Thread.sleep(1500L); 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); expireManager.receive(event);
Thread.sleep(1500L); Thread.sleep(1500L);
verify(eventPublisherMock, never()).post(any()); verify(eventPublisherMock, never()).post(any());

View File

@ -56,6 +56,8 @@ public class GenericItemTest {
@Test @Test
public void testItemPostsEventsCorrectly() { public void testItemPostsEventsCorrectly() {
ZonedDateTime lastStateUpdate;
ZonedDateTime lastStateChange;
EventPublisher publisher = mock(EventPublisher.class); EventPublisher publisher = mock(EventPublisher.class);
TestItem item = new TestItem("member1"); TestItem item = new TestItem("member1");
@ -78,6 +80,7 @@ public class GenericItemTest {
assertEquals(item.getName(), updated.getItemName()); assertEquals(item.getName(), updated.getItemName());
assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); assertEquals("openhab/items/member1/stateupdated", updated.getTopic());
assertEquals(item.getState(), updated.getItemState()); 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()); assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType());
// second event should be changed event // second event should be changed event
@ -87,12 +90,16 @@ public class GenericItemTest {
assertEquals("openhab/items/member1/statechanged", change.getTopic()); assertEquals("openhab/items/member1/statechanged", change.getTopic());
assertEquals(oldState, change.getOldItemState()); assertEquals(oldState, change.getOldItemState());
assertEquals(item.getState(), change.getItemState()); 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()); assertEquals(ItemStateChangedEvent.TYPE, change.getType());
// reset invocations and captor // reset invocations and captor
clearInvocations(publisher); clearInvocations(publisher);
captor = ArgumentCaptor.forClass(ItemEvent.class); captor = ArgumentCaptor.forClass(ItemEvent.class);
lastStateChange = item.getLastStateChange();
lastStateUpdate = item.getLastStateUpdate();
// State doesn't change -> only update event is fired // State doesn't change -> only update event is fired
item.setState(item.getState()); item.setState(item.getState());
verify(publisher).post(captor.capture()); verify(publisher).post(captor.capture());
@ -106,7 +113,25 @@ public class GenericItemTest {
assertEquals(item.getName(), updated.getItemName()); assertEquals(item.getName(), updated.getItemName());
assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); assertEquals("openhab/items/member1/stateupdated", updated.getTopic());
assertEquals(item.getState(), updated.getItemState()); assertEquals(item.getState(), updated.getItemState());
assertEquals(lastStateUpdate, updated.getLastStateUpdate());
assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); 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 @Test

View File

@ -16,6 +16,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.events.Event; import org.openhab.core.events.Event;
@ -225,8 +227,10 @@ public class ItemEventFactoryTest {
@Test @Test
public void testCreateGroupStateChangedEventRawType() throws Exception { public void testCreateGroupStateChangedEventRawType() throws Exception {
ZonedDateTime lastStateUpdate = ZonedDateTime.now();
ZonedDateTime lastStateChange = ZonedDateTime.now().minusMinutes(1);
GroupItemStateChangedEvent giEventSource = ItemEventFactory.createGroupStateChangedEvent(GROUP_NAME, ITEM_NAME, 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(), Event giEventParsed = factory.createEvent(giEventSource.getType(), giEventSource.getTopic(),
giEventSource.getPayload(), giEventSource.getSource()); giEventSource.getPayload(), giEventSource.getSource());
@ -242,5 +246,7 @@ public class ItemEventFactoryTest {
assertNull(groupItemStateChangedEvent.getSource()); assertNull(groupItemStateChangedEvent.getSource());
assertEquals(NEW_RAW_ITEM_STATE, groupItemStateChangedEvent.getItemState()); assertEquals(NEW_RAW_ITEM_STATE, groupItemStateChangedEvent.getItemState());
assertEquals(RAW_ITEM_STATE, groupItemStateChangedEvent.getOldItemState()); assertEquals(RAW_ITEM_STATE, groupItemStateChangedEvent.getOldItemState());
assertEquals(lastStateUpdate, groupItemStateChangedEvent.getLastStateUpdate());
assertEquals(lastStateChange, groupItemStateChangedEvent.getLastStateChange());
} }
} }

View File

@ -191,7 +191,7 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest {
logger.info("Rule is enabled and idle"); logger.info("Rule is enabled and idle");
logger.info("Send and wait for item state is ON"); 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(() -> { waitForAssert(() -> {
assertThat(itemEvent, is(notNullValue())); assertThat(itemEvent, is(notNullValue()));
@ -207,7 +207,7 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest {
// prepare the execution // prepare the execution
itemEvent = null; 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 Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing
assertThat(itemEvent, is(nullValue())); assertThat(itemEvent, is(nullValue()));
} }

View File

@ -168,7 +168,7 @@ public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
logger.info("Rule is enabled and idle"); logger.info("Rule is enabled and idle");
logger.info("Send and wait for item state is ON"); 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 // the first event is always processed
waitForAssert(() -> { waitForAssert(() -> {
@ -182,7 +182,7 @@ public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// Send a second event to check if the condition is still satisfied // Send a second event to check if the condition is still satisfied
itemEvent = null; // reset it itemEvent = null; // reset it
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null));
waitForAssert(() -> { waitForAssert(() -> {
assertThat(itemEvent, is(notNullValue())); assertThat(itemEvent, is(notNullValue()));
@ -198,7 +198,7 @@ public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// prepare the execution // prepare the execution
itemEvent = null; itemEvent = null;
eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON, null));
// the first event is always allowed // the first event is always allowed
waitForAssert(() -> { waitForAssert(() -> {
@ -209,7 +209,7 @@ public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// the second event is not allowed // the second event is not allowed
itemEvent = null; 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 Thread.sleep(200); // without this, the assertion will be immediately fulfilled regardless of event processing
assertThat(itemEvent, is(nullValue())); assertThat(itemEvent, is(nullValue()));
} }

View File

@ -360,7 +360,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test @Test
public void testItemStateEventSingleLink() { public void testItemStateEventSingleLink() {
manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON)); manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON, null));
waitForAssert(() -> { waitForAssert(() -> {
verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON));
verify(triggerProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock).onStateUpdateFromItem(eq(OnOffType.ON));
@ -371,7 +371,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test @Test
public void testItemStateEventMultiLink() { public void testItemStateEventMultiLink() {
manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON)); manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, null));
waitForAssert(() -> { waitForAssert(() -> {
verify(stateProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(stateProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON));
verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON));
@ -382,8 +382,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test @Test
public void testItemStateEventNotToSource() { public void testItemStateEventNotToSource() {
manager.receive( manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, null,
ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); STATE_CHANNEL_UID_2.getAsString()));
waitForAssert(() -> { waitForAssert(() -> {
verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON));
verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON));