From 4182980ec89357047b60a92f2b7f0b4ae7350260 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Fri, 31 Mar 2023 23:01:40 +0200 Subject: [PATCH] Add ItemStateUpdatedEvent and enable group channel-links (#3141) * Add (Group)ItemStateUpdatedEvent Signed-off-by: Jan N. Klug --- .../handler/ItemStateTriggerHandler.java | 8 +- .../thing/internal/CommunicationManager.java | 10 ++- .../org/openhab/core/items/GenericItem.java | 13 +++- .../org/openhab/core/items/GroupItem.java | 14 +++- .../events/GroupItemStateChangedEvent.java | 3 +- .../items/events/GroupStateUpdatedEvent.java | 58 ++++++++++++++ .../core/items/events/ItemEventFactory.java | 75 ++++++++++++++++++- .../core/items/events/ItemStateEvent.java | 4 +- .../items/events/ItemStateUpdatedEvent.java | 68 +++++++++++++++++ .../openhab/core/items/GenericItemTest.java | 41 ++++++++-- .../core/library/items/NumberItemTest.java | 53 +++++++++++++ .../internal/BasicConditionHandlerTest.java | 4 +- .../openhab/core/items/GroupItemOSGiTest.java | 63 +++++++++++----- .../CommunicationManagerOSGiTest.java | 8 +- 14 files changed, 374 insertions(+), 48 deletions(-) create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java index 702e19777..c931b4ca6 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java @@ -31,7 +31,7 @@ import org.openhab.core.items.events.GroupItemStateChangedEvent; import org.openhab.core.items.events.ItemAddedEvent; import org.openhab.core.items.events.ItemRemovedEvent; import org.openhab.core.items.events.ItemStateChangedEvent; -import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.types.State; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; @@ -77,7 +77,7 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements this.previousState = (String) module.getConfiguration().get(CFG_PREVIOUS_STATE); this.ruleUID = ruleUID; if (UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { - this.types = Set.of(ItemStateEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE); + this.types = Set.of(ItemStateUpdatedEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE); } else { this.types = Set.of(ItemStateChangedEvent.TYPE, GroupItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE); @@ -122,9 +122,9 @@ public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), event.getTopic(), event.getType(), event.getPayload()); Map values = new HashMap<>(); - if (event instanceof ItemStateEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { + if (event instanceof ItemStateUpdatedEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { String state = this.state; - State itemState = ((ItemStateEvent) event).getItemState(); + State itemState = ((ItemStateUpdatedEvent) event).getItemState(); if ((state == null || state.equals(itemState.toFullString()))) { values.put("state", itemState); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java index b41c1ca50..e2b3a9b8b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java @@ -41,8 +41,10 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemStateConverter; import org.openhab.core.items.ItemUtil; import org.openhab.core.items.events.AbstractItemRegistryEvent; +import org.openhab.core.items.events.GroupStateUpdatedEvent; import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; @@ -115,7 +117,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList public static final long THINGHANDLER_EVENT_TIMEOUT = TimeUnit.SECONDS.toMillis(30); private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE, - ChannelTriggeredEvent.TYPE); + GroupStateUpdatedEvent.TYPE, ChannelTriggeredEvent.TYPE); private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class); @@ -179,8 +181,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList @Override public void receive(Event event) { - if (event instanceof ItemStateEvent) { - receiveUpdate((ItemStateEvent) event); + if (event instanceof ItemStateUpdatedEvent) { + receiveUpdate((ItemStateUpdatedEvent) event); } else if (event instanceof ItemCommandEvent) { receiveCommand((ItemCommandEvent) event); } else if (event instanceof ChannelTriggeredEvent) { @@ -358,7 +360,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList }); } - private void receiveUpdate(ItemStateEvent updateEvent) { + private void receiveUpdate(ItemStateUpdatedEvent updateEvent) { final String itemName = updateEvent.getItemName(); final State newState = updateEvent.getItemState(); handleEvent(itemName, newState, updateEvent.getSource(), s -> acceptedStateTypeMap.get(s), diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java index 5e7c3366b..dd0a53ec2 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java @@ -231,14 +231,23 @@ public abstract class GenericItem implements ActiveItem { State oldState = this.state; this.state = state; notifyListeners(oldState, state); + sendStateUpdatedEvent(state); if (!oldState.equals(state)) { sendStateChangedEvent(state, oldState); } } + private void sendStateUpdatedEvent(State newState) { + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createStateUpdatedEvent(this.name, newState, null)); + } + } + private void sendStateChangedEvent(State newState, State oldState) { - if (eventPublisher != null) { - eventPublisher.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java index 2125ed7ac..546d435fc 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.events.EventPublisher; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.service.CommandDescriptionService; @@ -371,6 +372,7 @@ public class GroupItem extends GenericItem implements StateChangeListener { State calculatedState = function.calculate(getStateMembers(getMembers())); newState = itemStateConverter.convertToAcceptedState(calculatedState, baseItem); setState(newState); + sendGroupStateUpdatedEvent(item.getName(), newState); } if (!oldState.equals(newState)) { sendGroupStateChangedEvent(item.getName(), newState, oldState); @@ -413,9 +415,17 @@ public class GroupItem extends GenericItem implements StateChangeListener { } } + private void sendGroupStateUpdatedEvent(String memberName, State state) { + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createGroupStateEvent(getName(), memberName, state, null)); + } + } + private void sendGroupStateChangedEvent(String memberName, State newState, State oldState) { - if (eventPublisher != null) { - eventPublisher + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1 .post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState, oldState)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java index 171aae7ff..0545c5f15 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java @@ -17,7 +17,8 @@ import org.openhab.core.types.State; /** * {@link GroupItemStateChangedEvent}s can be used to deliver group item state changes through the openHAB event bus. In - * contrast to the {@link GroupItemStateEvent} the {@link GroupItemStateChangedEvent} is only sent if the state changed. + * contrast to the {@link GroupStateUpdatedEvent} the {@link GroupItemStateChangedEvent} is only sent if the state + * changed. * State events must be created with the {@link ItemEventFactory}. * * @author Christoph Knauf - Initial contribution diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java new file mode 100644 index 000000000..9a1d8d773 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.items.events; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +/** + * {@link GroupStateUpdatedEvent}s can be used to deliver group item state updates through the openHAB event bus. + * In contrast to the {@link GroupItemStateChangedEvent} it is always sent. + * State events must be created with the {@link ItemEventFactory}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class GroupStateUpdatedEvent extends ItemStateUpdatedEvent { + + /** + * The group item state changed event type. + */ + public static final String TYPE = GroupStateUpdatedEvent.class.getSimpleName(); + + 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); + this.memberName = memberName; + } + + /** + * @return the name of the changed group member + */ + public String getMemberName() { + return this.memberName; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return String.format("Group '%s' updated to %s through %s", itemName, itemState, memberName); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java index b380be727..ccf1cabab 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java @@ -50,10 +50,14 @@ public class ItemEventFactory extends AbstractEventFactory { private static final String ITEM_STATE_EVENT_TOPIC = "openhab/items/{itemName}/state"; + private static final String ITEM_STATE_UPDATED_EVENT_TOPIC = "openhab/items/{itemName}/stateupdated"; + private static final String ITEM_STATE_PREDICTED_EVENT_TOPIC = "openhab/items/{itemName}/statepredicted"; private static final String ITEM_STATE_CHANGED_EVENT_TOPIC = "openhab/items/{itemName}/statechanged"; + private static final String GROUP_STATE_EVENT_TOPIC = "openhab/items/{itemName}/{memberName}/stateupdated"; + private static final String GROUPITEM_STATE_CHANGED_EVENT_TOPIC = "openhab/items/{itemName}/{memberName}/statechanged"; private static final String ITEM_ADDED_EVENT_TOPIC = "openhab/items/{itemName}/added"; @@ -67,8 +71,8 @@ public class ItemEventFactory extends AbstractEventFactory { */ public ItemEventFactory() { super(Set.of(ItemCommandEvent.TYPE, ItemStateEvent.TYPE, ItemStatePredictedEvent.TYPE, - ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, ItemRemovedEvent.TYPE, - GroupItemStateChangedEvent.TYPE)); + ItemStateUpdatedEvent.TYPE, ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, + ItemRemovedEvent.TYPE, GroupStateUpdatedEvent.TYPE, GroupItemStateChangedEvent.TYPE)); } @Override @@ -80,6 +84,8 @@ public class ItemEventFactory extends AbstractEventFactory { return createStateEvent(topic, payload, source); } else if (ItemStatePredictedEvent.TYPE.equals(eventType)) { return createStatePredictedEvent(topic, payload); + } else if (ItemStateUpdatedEvent.TYPE.equals(eventType)) { + return createStateUpdatedEvent(topic, payload); } else if (ItemStateChangedEvent.TYPE.equals(eventType)) { return createStateChangedEvent(topic, payload); } else if (ItemAddedEvent.TYPE.equals(eventType)) { @@ -88,12 +94,22 @@ public class ItemEventFactory extends AbstractEventFactory { return createUpdatedEvent(topic, payload); } else if (ItemRemovedEvent.TYPE.equals(eventType)) { return createRemovedEvent(topic, payload); + } else if (GroupStateUpdatedEvent.TYPE.equals(eventType)) { + return createGroupStateEvent(topic, payload); } else if (GroupItemStateChangedEvent.TYPE.equals(eventType)) { return createGroupStateChangedEvent(topic, payload); } throw new IllegalArgumentException("The event type '" + eventType + "' is not supported by this factory."); } + private Event createGroupStateEvent(String topic, String payload) { + String itemName = getItemName(topic); + String memberName = getMemberName(topic); + ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + State state = getState(bean.getType(), bean.getValue()); + return new GroupStateUpdatedEvent(topic, payload, itemName, memberName, state, null); + } + private Event createGroupStateChangedEvent(String topic, String payload) { String itemName = getItemName(topic); String memberName = getMemberName(topic); @@ -124,6 +140,13 @@ public class ItemEventFactory extends AbstractEventFactory { return new ItemStatePredictedEvent(topic, payload, itemName, state, bean.isConfirmation()); } + private Event createStateUpdatedEvent(String topic, String payload) { + String itemName = getItemName(topic); + ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + State state = getState(bean.getType(), bean.getValue()); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, null); + } + private Event createStateChangedEvent(String topic, String payload) { String itemName = getItemName(topic); ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); @@ -268,6 +291,54 @@ public class ItemEventFactory extends AbstractEventFactory { return createStateEvent(itemName, state, null); } + /** + * Creates an item state updated event. + * + * @param itemName the name of the item to report the state update for + * @param state the new state + * @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); + } + + /** + * Creates an item state updated event. + * + * @param itemName the name of the item to report the state update for + * @param state the new state + * @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) { + assertValidArguments(itemName, state, "state"); + String topic = buildTopic(ITEM_STATE_UPDATED_EVENT_TOPIC, itemName); + ItemEventPayloadBean bean = new ItemEventPayloadBean(getStateType(state), state.toFullString()); + String payload = serializePayload(bean); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, source); + } + + /** + * Creates an group item state updated event. + * + * @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 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 createGroupStateEvent(String groupName, String member, State state, + @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()); + String payload = serializePayload(bean); + return new GroupStateUpdatedEvent(topic, payload, groupName, member, state, source); + } + /** * Creates an item state predicted event. * diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java index 80865370c..90d6eb326 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java @@ -30,7 +30,7 @@ public class ItemStateEvent extends ItemEvent { */ public static final String TYPE = ItemStateEvent.class.getSimpleName(); - private final State itemState; + protected final State itemState; /** * Constructs a new item state event. @@ -62,6 +62,6 @@ public class ItemStateEvent extends ItemEvent { @Override public String toString() { - return String.format("Item '%s' updated to %s", itemName, itemState); + return String.format("Item '%s' shall update to %s", itemName, itemState); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java new file mode 100644 index 000000000..0b582c282 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.items.events; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +/** + * {@link ItemStateUpdatedEvent}s can be used to report item status updates through the openHAB event bus. + * State update events must be created with the {@link ItemEventFactory}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ItemStateUpdatedEvent extends ItemEvent { + + /** + * The item state event type. + */ + public static final String TYPE = ItemStateUpdatedEvent.class.getSimpleName(); + + protected final State itemState; + + /** + * Constructs a new item state event. + * + * @param topic the topic + * @param payload the payload + * @param itemName the item name + * @param itemState the item state + * @param source the source, can be null + */ + protected ItemStateUpdatedEvent(String topic, String payload, String itemName, State itemState, + @Nullable String source) { + super(topic, payload, itemName, source); + this.itemState = itemState; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * Gets the item state. + * + * @return the item state + */ + public State getItemState() { + return itemState; + } + + @Override + public String toString() { + return String.format("Item '%s' updated to %s", itemName, itemState); + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java index 370b86025..8a60f9ec7 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java @@ -27,7 +27,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.openhab.core.events.EventPublisher; import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.items.events.ItemEvent; import org.openhab.core.items.events.ItemStateChangedEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.RawType; @@ -58,24 +60,51 @@ public class GenericItemTest { item.setEventPublisher(publisher); State oldState = item.getState(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired item.setState(new RawType(new byte[0], RawType.DEFAULT_MIME_TYPE)); - ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStateChangedEvent.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemEvent.class); - verify(publisher, times(1)).post(captor.capture()); + verify(publisher, times(2)).post(captor.capture()); - ItemStateChangedEvent change = captor.getValue(); + List events = captor.getAllValues(); + assertEquals(2, events.size()); + // first event should be updated event + assertInstanceOf(ItemStateUpdatedEvent.class, events.get(0)); + ItemStateUpdatedEvent updated = (ItemStateUpdatedEvent) events.get(0); + assertEquals(item.getName(), updated.getItemName()); + assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); + assertEquals(item.getState(), updated.getItemState()); + assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); + + // second event should be changed event + assertInstanceOf(ItemStateChangedEvent.class, events.get(1)); + ItemStateChangedEvent change = (ItemStateChangedEvent) events.get(1); assertEquals(item.getName(), change.getItemName()); assertEquals("openhab/items/member1/statechanged", change.getTopic()); assertEquals(oldState, change.getOldItemState()); assertEquals(item.getState(), change.getItemState()); assertEquals(ItemStateChangedEvent.TYPE, change.getType()); - // State doesn't change -> no event is fired + // reset invocations and captor + clearInvocations(publisher); + captor = ArgumentCaptor.forClass(ItemEvent.class); + + // State doesn't change -> only update event is fired item.setState(item.getState()); - verifyNoMoreInteractions(publisher); + verify(publisher).post(captor.capture()); + + events = captor.getAllValues(); + assertEquals(1, events.size()); // two before and one additional + + // event should be updated event + assertInstanceOf(ItemStateUpdatedEvent.class, events.get(0)); + updated = (ItemStateUpdatedEvent) events.get(0); + assertEquals(item.getName(), updated.getItemName()); + assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); + assertEquals(item.getState(), updated.getItemState()); + assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); } @Test diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java index 147c85ba6..6d4612ac1 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java @@ -14,13 +14,18 @@ package org.openhab.core.library.items; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; +import java.util.List; + import javax.measure.quantity.Energy; import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,9 +34,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.events.ItemCommandEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.PercentType; @@ -57,12 +64,15 @@ public class NumberItemTest { private static final String ITEM_NAME = "test"; private @Mock @NonNullByDefault({}) StateDescriptionService stateDescriptionServiceMock; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock; @BeforeEach public void setup() { when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)) .thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.1f " + UnitUtils.UNIT_PLACEHOLDER) .build().toStateDescription()); + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); } @Test @@ -286,4 +296,47 @@ public class NumberItemTest { assertThat(item.getState(), is(new QuantityType<>("329 kWh"))); assertThat(item.getUnit(), is(nullValue())); } + + public void quantityTypeCorrectlySetWithDifferentUnit() { + NumberItem numberItem = new NumberItem("Number:Temperature", ITEM_NAME); + numberItem.setUnitProvider(unitProviderMock); + numberItem.setEventPublisher(eventPublisherMock); + numberItem.setState(new QuantityType<>("140 °F")); + + assertThat(numberItem.getState(), Matchers.is(new QuantityType<>("60 °C"))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Event.class); + verify(eventPublisherMock, times(2)).post(captor.capture()); + + List events = captor.getAllValues(); + assertThat(events, hasSize(2)); + + assertThat(events.get(0), Matchers.is(instanceOf(ItemStateUpdatedEvent.class))); + + ItemStateUpdatedEvent updatedEvent = (ItemStateUpdatedEvent) events.get(0); + assertThat(updatedEvent.getItemName(), Matchers.is(ITEM_NAME)); + assertThat(updatedEvent.getItemState(), Matchers.is(new QuantityType<>("60°C"))); + } + + @Test + public void decimalTypeCorrectlySetWithUnit() { + NumberItem numberItem = new NumberItem("Number:Temperature", ITEM_NAME); + numberItem.setUnitProvider(unitProviderMock); + numberItem.setEventPublisher(eventPublisherMock); + numberItem.setState(new DecimalType(10)); + + assertThat(numberItem.getState(), Matchers.is(new QuantityType<>("10 °C"))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Event.class); + verify(eventPublisherMock, times(2)).post(captor.capture()); + + List events = captor.getAllValues(); + assertThat(events, hasSize(2)); + + assertThat(events.get(0), Matchers.is(instanceOf(ItemStateUpdatedEvent.class))); + + ItemStateUpdatedEvent updatedEvent = (ItemStateUpdatedEvent) events.get(0); + assertThat(updatedEvent.getItemName(), Matchers.is(ITEM_NAME)); + assertThat(updatedEvent.getItemState(), Matchers.is(new QuantityType<>("10°C"))); + } } diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java index 2d04b921f..3a06424ba 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java @@ -179,7 +179,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.createStateEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); waitForAssert(() -> { assertThat(itemEvent, is(notNullValue())); @@ -194,7 +194,7 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest { // prepare the execution itemEvent = null; - eventPublisher.post(ItemEventFactory.createStateEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); waitForAssert(() -> { assertThat(itemEvent, is(nullValue())); }); diff --git a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java index 19501fc7e..e1d8afe2c 100644 --- a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java +++ b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java @@ -46,7 +46,9 @@ import org.openhab.core.internal.items.GroupFunctionHelper; import org.openhab.core.internal.items.ItemStateConverterImpl; import org.openhab.core.items.dto.GroupFunctionDTO; import org.openhab.core.items.events.GroupItemStateChangedEvent; +import org.openhab.core.items.events.GroupStateUpdatedEvent; import org.openhab.core.items.events.ItemCommandEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.items.events.ItemUpdatedEvent; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.items.ColorItem; @@ -422,6 +424,7 @@ public class GroupItemOSGiTest extends JavaOSGiTest { public void assertThatGroupItemPostsEventsForChangesCorrectly() { // from ItemEventFactory.GROUPITEM_STATE_CHANGED_EVENT_TOPIC String groupitemStateChangedEventTopic = "openhab/items/{itemName}/{memberName}/statechanged"; + String groupitemStateUpdatedEventTopic = "openhab/items/{itemName}/{memberName}/stateupdated"; events.clear(); GroupItem groupItem = new GroupItem("root", new SwitchItem("mySwitch"), new GroupFunction.Equality()); @@ -432,10 +435,21 @@ public class GroupItemOSGiTest extends JavaOSGiTest { groupItem.setEventPublisher(publisher); State oldGroupState = groupItem.getState(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired member.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); + + List updates = events.stream().filter(it -> it instanceof GroupStateUpdatedEvent) + .collect(Collectors.toList()); + assertThat(updates.size(), is(1)); + + GroupStateUpdatedEvent update = (GroupStateUpdatedEvent) updates.get(0); + assertThat(update.getItemName(), is(groupItem.getName())); + assertThat(update.getMemberName(), is(member.getName())); + assertThat(update.getTopic(), is(groupitemStateUpdatedEventTopic.replace("{memberName}", member.getName()) + .replace("{itemName}", groupItem.getName()))); + assertThat(update.getItemState(), is(groupItem.getState())); List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) .collect(Collectors.toList()); @@ -451,9 +465,10 @@ public class GroupItemOSGiTest extends JavaOSGiTest { events.clear(); - // State doesn't change -> no events are fired + // State doesn't change -> only update event is posted member.setState(member.getState()); - assertThat(events.size(), is(0)); + waitForAssert(() -> assertThat(events.size(), is(1))); + assertThat(events.get(0), is(instanceOf(ItemStateUpdatedEvent.class))); } @Test @@ -470,10 +485,10 @@ public class GroupItemOSGiTest extends JavaOSGiTest { groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); List groupItemStateChangedEvents = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) .collect(Collectors.toList()); @@ -490,13 +505,13 @@ public class GroupItemOSGiTest extends JavaOSGiTest { events.clear(); - // State does not change -> no change event is fired + // State does not change -> only update event is fired sw2.setState(OnOffType.ON); // wait to see that the event doesn't fire Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - waitForAssert(() -> assertThat(events, hasSize(0))); + waitForAssert(() -> assertThat(events, hasSize(1))); } @Test @@ -543,15 +558,19 @@ public class GroupItemOSGiTest extends JavaOSGiTest { groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) .collect(Collectors.toList()); assertThat(changes, hasSize(1)); + List updates = events.stream().filter(it -> it instanceof GroupStateUpdatedEvent) + .collect(Collectors.toList()); + assertThat(updates, hasSize(1)); + GroupItemStateChangedEvent change = (GroupItemStateChangedEvent) changes.get(0); assertThat(change.getItemName(), is(groupItem.getName())); @@ -563,15 +582,21 @@ public class GroupItemOSGiTest extends JavaOSGiTest { events.clear(); - // State does not change -> no change event is fired + // State does not change -> only update event is fired sw2.setState(OnOffType.ON); sw2.setState(UnDefType.UNDEF); - // wait to see that the event doesn't fire + // wait to see that only state updated events are fired Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - assertThat(events, hasSize(0)); + assertThat(events, hasSize(2)); + + changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent).collect(Collectors.toList()); + assertThat(changes, hasSize(0)); + + updates = events.stream().filter(it -> it instanceof GroupStateUpdatedEvent).collect(Collectors.toList()); + assertThat(updates, hasSize(2)); assertThat(groupItem.getState(), is(OnOffType.ON)); } @@ -590,10 +615,10 @@ public class GroupItemOSGiTest extends JavaOSGiTest { groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) .collect(Collectors.toList()); @@ -610,10 +635,10 @@ public class GroupItemOSGiTest extends JavaOSGiTest { events.clear(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw2.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent).collect(Collectors.toList()); assertThat(changes, hasSize(1)); @@ -727,7 +752,7 @@ public class GroupItemOSGiTest extends JavaOSGiTest { member1.setState(new PercentType(50)); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) .collect(Collectors.toList()); @@ -746,7 +771,7 @@ public class GroupItemOSGiTest extends JavaOSGiTest { member2.setState(new PercentType(10)); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent).collect(Collectors.toList()); assertThat(changes.size(), is(1)); diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java index 3d4caccf0..faf25f40b 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java @@ -351,7 +351,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest { @Test public void testItemStateEventSingleLink() { - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_2, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON)); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -362,7 +362,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest { @Test public void testItemStateEventMultiLink() { - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_1, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON)); waitForAssert(() -> { verify(stateProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -374,7 +374,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest { @Test public void testItemStateEventNotToSource() { manager.receive( - ItemEventFactory.createStateEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); + ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -596,7 +596,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest { thing.setHandler(thingHandlerMock); when(thingRegistryMock.get(eq(THING_UID))).thenReturn(thing); - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_2, HSBType.fromRGB(128, 128, 128))); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, HSBType.fromRGB(128, 128, 128))); waitForAssert(() -> { ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class); verify(stateProfileMock).onStateUpdateFromItem(stateCaptor.capture());