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)) {
State state = isEvent.getItemState();
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) {
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 class GroupStateTriggerHandler extends BaseTriggerModuleHandler implement
State oldState = iscEvent.getOldItemState();
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) {
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);
}

View File

@ -132,13 +132,14 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements
if (callback != null) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload());
Map<String, Object> values = new HashMap<>();
Map<String, @Nullable Object> 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())) {
@ -148,6 +149,8 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements
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()) {

View File

@ -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",

View File

@ -258,7 +258,8 @@ public class EventWebSocketTest {
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 class EventWebSocketTest {
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 class EventWebSocketTest {
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)));

View File

@ -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 = typeRef(Command)
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)) {
val stateTypeRef = typeRef(State)
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)) {
val eventTypeRef = typeRef(String)
parameters += rule.toParameter(VAR_RECEIVED_EVENT, eventTypeRef)
@ -163,10 +176,6 @@ class RulesJvmModelInferrer extends ScriptJvmModelInferrer {
val newStatusRef = typeRef(String)
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
]

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";
private static final Map<String, String> 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<String, String> 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);

View File

@ -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 = typeRef(State)
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)
parameters += script.toParameter(VAR_PRIVATE_CACHE, privateCacheTypeRef)
val sharedCacheTypeRef = typeRef(ValueCache)

View File

@ -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<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.
@ -120,4 +129,25 @@ public abstract class AbstractEventFactory implements EventFactory {
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,
@Nullable ZonedDateTime lastStateChange) {
State oldState = this.state;
ZonedDateTime oldStateUpdate = this.lastStateUpdate;
this.state = state;
this.lastState = lastState != null ? lastState : this.lastState;
this.lastStateUpdate = lastStateUpdate != null ? lastStateUpdate : this.lastStateUpdate;
this.lastStateChange = lastStateChange != null ? lastStateChange : this.lastStateChange;
notifyListeners(oldState, state);
sendStateUpdatedEvent(state);
sendStateUpdatedEvent(state, lastStateUpdate);
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
}
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;
@ -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;
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));
}
}

View File

@ -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;
@ -365,19 +366,22 @@ public class GroupItem extends GenericItem implements StateChangeListener, Metad
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) {
@ -387,6 +391,10 @@ public class GroupItem extends GenericItem implements StateChangeListener, Metad
this.state = state;
}
notifyListeners(oldState, state);
if (!oldState.equals(state)) {
lastStateChange = now;
}
lastStateUpdate = now;
}
@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;
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));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -15,6 +15,7 @@ package org.openhab.core.items.events;
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 @@ public class ItemEventFactory extends AbstractEventFactory {
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 @@ public class ItemEventFactory extends AbstractEventFactory {
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 @@ public class ItemEventFactory extends AbstractEventFactory {
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 @@ public class ItemEventFactory extends AbstractEventFactory {
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 class ItemEventFactory extends AbstractEventFactory {
*
* @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 class ItemEventFactory extends AbstractEventFactory {
*
* @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 class ItemEventFactory extends AbstractEventFactory {
* @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 class ItemEventFactory extends AbstractEventFactory {
* @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 class ItemEventFactory extends AbstractEventFactory {
* @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 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.
*/
@ -593,6 +651,8 @@ public class ItemEventFactory extends AbstractEventFactory {
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 @@ public class ItemEventFactory extends AbstractEventFactory {
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 class ItemEventFactory extends AbstractEventFactory {
public String getOldValue() {
return oldValue;
}
public @Nullable ZonedDateTime getLastStateUpdate() {
return lastStateUpdate;
}
public @Nullable ZonedDateTime getLastStateChange() {
return lastStateChange;
}
}
private static class ItemTimeSeriesEventPayloadBean {

View File

@ -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 class ItemStateChangedEvent extends ItemEvent {
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);

View File

@ -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 class ItemStateUpdatedEvent extends ItemEvent {
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);

View File

@ -142,7 +142,7 @@ class ExpireManagerTest {
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 @@ class ExpireManagerTest {
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());

View File

@ -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 class GenericItemTest {
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 class GenericItemTest {
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 class GenericItemTest {
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

View File

@ -16,6 +16,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
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;
@ -225,8 +227,10 @@ public class ItemEventFactoryTest {
@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());
@ -242,5 +246,7 @@ public class ItemEventFactoryTest {
assertNull(groupItemStateChangedEvent.getSource());
assertEquals(NEW_RAW_ITEM_STATE, groupItemStateChangedEvent.getItemState());
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("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 abstract class BasicConditionHandlerTest extends JavaOSGiTest {
// 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()));
}

View File

@ -168,7 +168,7 @@ public class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
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 class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// 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 class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// 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 class IntervalConditionHandlerTest extends BasicConditionHandlerTest {
// 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()));
}

View File

@ -360,7 +360,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@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 class CommunicationManagerOSGiTest extends JavaOSGiTest {
@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 class CommunicationManagerOSGiTest extends JavaOSGiTest {
@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));