From 44e0e1edced625b8a2a77ff0eed672d2095ad282 Mon Sep 17 00:00:00 2001 From: altaroca <8048513+altaroca@users.noreply.github.com> Date: Mon, 13 Jun 2022 21:48:47 +0200 Subject: [PATCH 1/2] Enable binding to store historic states --- .../internal/PersistenceManagerImpl.java | 44 +++++++++- .../core/thing/binding/BaseThingHandler.java | 34 ++++++++ .../thing/binding/ThingHandlerCallback.java | 10 +++ .../thing/internal/CommunicationManager.java | 11 +++ .../core/thing/internal/HistoricState.java | 52 ++++++++++++ .../core/thing/internal/ThingManagerImpl.java | 6 ++ .../profiles/ProfileCallbackImpl.java | 21 ++++- .../core/internal/items/ItemUpdater.java | 40 ++++++++++ .../org/openhab/core/items/GenericItem.java | 41 ++++++++++ .../items/HistoricStateChangeListener.java | 39 +++++++++ .../events/AbstractItemEventSubscriber.java | 15 +++- .../core/items/events/ItemEventFactory.java | 73 ++++++++++++++++- .../items/events/ItemHistoricStateEvent.java | 80 +++++++++++++++++++ 13 files changed, 458 insertions(+), 8 deletions(-) create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java index 7f88490e298..22df11a920f 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java @@ -12,6 +12,7 @@ */ package org.openhab.core.persistence.internal; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Collections; @@ -31,13 +32,14 @@ import org.openhab.core.common.SafeCaller; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; +import org.openhab.core.items.HistoricStateChangeListener; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistryChangeListener; -import org.openhab.core.items.StateChangeListener; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.ModifiablePersistenceService; import org.openhab.core.persistence.PersistenceItemConfiguration; import org.openhab.core.persistence.PersistenceManager; import org.openhab.core.persistence.PersistenceService; @@ -76,7 +78,7 @@ @Component(immediate = true, service = PersistenceManager.class) @NonNullByDefault public class PersistenceManagerImpl - implements ItemRegistryChangeListener, PersistenceManager, StateChangeListener, ReadyTracker { + implements ItemRegistryChangeListener, PersistenceManager, HistoricStateChangeListener, ReadyTracker { private final Logger logger = LoggerFactory.getLogger(PersistenceManagerImpl.class); @@ -158,6 +160,39 @@ private void handleStateEvent(Item item, boolean onlyChanges) { } } + /** + * Calls all persistence services which use change or update policy for the given item + * + * @param item the item to persist + * @param onlyChanges true, if it has the change strategy, false otherwise + */ + private void handleHistoricStateEvent(Item item, State state, ZonedDateTime dateTime, boolean onlyChanges) { + logger.debug("Persisting item '{}' historic state '{}' at {}", item.getName(), state.toString(), dateTime.toString()); + synchronized (persistenceServiceConfigs) { + for (Entry entry : persistenceServiceConfigs + .entrySet()) { + final String serviceName = entry.getKey(); + final PersistenceServiceConfiguration config = entry.getValue(); + if (config != null && persistenceServices.containsKey(serviceName) + && persistenceServices.get(serviceName) instanceof ModifiablePersistenceService) { + ModifiablePersistenceService service = (ModifiablePersistenceService) persistenceServices + .get(serviceName); + logger.debug(" Using ModifiablePersistenceService '{}'", serviceName); + for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { + if (hasStrategy(config, itemConfig, onlyChanges ? PersistenceStrategy.Globals.CHANGE + : PersistenceStrategy.Globals.UPDATE)) { + logger.debug(" trying ItemConfig '{}'", itemConfig.toString()); + if (appliesToItem(itemConfig, item)) { + logger.debug(" config applies"); // false ?? + service.store(item, dateTime, state); + } + } + } + } + } + } + } + /** * Checks if a given persistence configuration entry has a certain strategy for the given service * @@ -478,6 +513,11 @@ public void stateUpdated(Item item, State state) { handleStateEvent(item, false); } + @Override + public void historicStateUpdated(Item item, State state, ZonedDateTime dateTime) { + handleHistoricStateEvent(item, state, dateTime, false); + } + @Override public void onReadyMarkerAdded(ReadyMarker readyMarker) { ExecutorService scheduler = Executors.newSingleThreadExecutor(new NamedThreadFactory("persistenceManager")); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java index 34f948f491b..e0de9865043 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.core.thing.binding; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -287,6 +288,39 @@ protected void updateState(String channelID, State state) { updateState(channelUID, state); } + /** + * + * Updates a historic state of the thing. + * + * @param channelUID unique id of the channel, which was updated + * @param state new state + */ + protected void updateHistoricState(ChannelUID channelUID, State state, ZonedDateTime dateTime) { + synchronized (this) { + if (this.callback != null) { + // TODO: check if this historic state is later than any existing state + this.callback.historicStateUpdated(channelUID, state, dateTime); + } else { + logger.warn( + "Handler {} of thing {} tried updating channel {} although the handler was already disposed.", + this.getClass().getSimpleName(), channelUID.getThingUID(), channelUID.getId()); + } + } + } + + /** + * + * Updates a historic state of the thing. Will use the thing UID to infer the + * unique channel UID from the given ID. + * + * @param channelID id of the channel, which was updated + * @param state new state + */ + protected void updateHistoricState(String channelID, State state, ZonedDateTime dateTime) { + ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelID); + updateHistoricState(channelUID, state, dateTime); + } + /** * Emits an event for the given channel. * diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java index bbad69fb362..5137724c27e 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java @@ -12,6 +12,7 @@ */ package org.openhab.core.thing.binding; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -57,6 +58,15 @@ public interface ThingHandlerCallback { */ void stateUpdated(ChannelUID channelUID, State state); + /** + * Informs about an update to a historic state for a channel. + * + * @param channelUID channel UID (must not be null) + * @param state state (must not be null) + * @param dateTime date time (must not be null) + */ + void historicStateUpdated(ChannelUID channelUID, State state, ZonedDateTime dateTime); + /** * Informs about a command, which is sent from the channel. * 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 3640c3a33fd..3a36669a9b9 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 @@ -13,6 +13,7 @@ package org.openhab.core.thing.internal; import java.time.Duration; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -568,6 +569,16 @@ public void stateUpdated(ChannelUID channelUID, State state) { }); } + public void historicStateUpdated(ChannelUID channelUID, State state, ZonedDateTime dateTime) { + final Thing thing = getThing(channelUID.getThingUID()); + + handleCallFromHandler(channelUID, thing, profile -> { + if (profile instanceof StateProfile) { + ((StateProfile) profile).onStateUpdateFromHandler(new HistoricState(state, dateTime)); + } + }); + } + public void postCommand(ChannelUID channelUID, Command command) { final Thing thing = getThing(channelUID.getThingUID()); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java new file mode 100644 index 00000000000..60b4786aa24 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2022 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.thing.internal; + +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +public class HistoricState implements State { + + private State state; + private ZonedDateTime dateTime; + + HistoricState(State state, ZonedDateTime dateTime) { + this.state = state; + this.dateTime = dateTime; + } + + public State getState() { + return state; + } + + public ZonedDateTime getDateTime() { + return dateTime; + } + + @Override + public String format(String pattern) { + return state.format(pattern); + } + + @Override + public String toFullString() { + return state.toFullString(); + } + + @Override + public @Nullable T as(@Nullable Class target) { + return state.as(target); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java index ea0e03eef5b..04fb4ebfcac 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java @@ -16,6 +16,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -176,6 +177,11 @@ public void stateUpdated(ChannelUID channelUID, State state) { communicationManager.stateUpdated(channelUID, state); } + @Override + public void historicStateUpdated(ChannelUID channelUID, State state, ZonedDateTime dateTime) { + communicationManager.historicStateUpdated(channelUID, state, dateTime); + } + @Override public void postCommand(ChannelUID channelUID, Command command) { communicationManager.postCommand(channelUID, command); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/ProfileCallbackImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/ProfileCallbackImpl.java index 1505db7803e..3c14442ebdb 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/ProfileCallbackImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/ProfileCallbackImpl.java @@ -12,6 +12,7 @@ */ package org.openhab.core.thing.internal.profiles; +import java.time.ZonedDateTime; import java.util.function.Function; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -20,6 +21,7 @@ import org.openhab.core.events.EventPublisher; import org.openhab.core.items.Item; import org.openhab.core.items.ItemStateConverter; +import org.openhab.core.items.events.ItemEvent; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.types.StringType; @@ -27,6 +29,7 @@ import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.internal.CommunicationManager; +import org.openhab.core.thing.internal.HistoricState; import org.openhab.core.thing.link.ItemChannelLink; import org.openhab.core.thing.profiles.ProfileCallback; import org.openhab.core.thing.util.ThingHandlerHelper; @@ -111,6 +114,12 @@ public void sendUpdate(State state) { return; } + ZonedDateTime dateTime = null; + if (state instanceof HistoricState) { + dateTime = ((HistoricState) state).getDateTime(); + state = ((HistoricState) state).getState(); + } + State acceptedState; if (state instanceof StringType && !(item instanceof StringItem)) { acceptedState = TypeParser.parseState(item.getAcceptedDataTypes(), state.toString()); @@ -121,7 +130,15 @@ public void sendUpdate(State state) { acceptedState = itemStateConverter.convertToAcceptedState(state, item); } - eventPublisher.post( - ItemEventFactory.createStateEvent(link.getItemName(), acceptedState, link.getLinkedUID().toString())); + ItemEvent event; + if (dateTime != null) { + event = ItemEventFactory.createHistoricStateEvent(link.getItemName(), acceptedState, dateTime, + link.getLinkedUID().toString()); + } else { + event = ItemEventFactory.createStateEvent(link.getItemName(), acceptedState, + link.getLinkedUID().toString()); + } + + eventPublisher.post(event); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java index 8a0617184ca..1b495912cdc 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java @@ -12,6 +12,8 @@ */ package org.openhab.core.internal.items; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.events.EventSubscriber; import org.openhab.core.items.GenericItem; @@ -21,6 +23,7 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.AbstractItemEventSubscriber; import org.openhab.core.items.events.ItemCommandEvent; +import org.openhab.core.items.events.ItemHistoricStateEvent; import org.openhab.core.items.events.ItemStateEvent; import org.openhab.core.types.State; import org.osgi.service.component.annotations.Activate; @@ -97,4 +100,41 @@ protected void receiveCommand(ItemCommandEvent commandEvent) { logger.debug("Received command for non-existing item: {}", e.getMessage()); } } + + @Override + protected void receiveHistoricState(ItemHistoricStateEvent event) { + String itemName = event.getItemName(); + State newState = event.getItemState(); + ZonedDateTime dateTime = event.getDateTime(); + try { + GenericItem item = (GenericItem) itemRegistry.getItem(itemName); + boolean isAccepted = false; + if (item.getAcceptedDataTypes().contains(newState.getClass())) { + isAccepted = true; + } else { + // Look for class hierarchy + for (Class state : item.getAcceptedDataTypes()) { + try { + if (!state.isEnum() && state.getDeclaredConstructor().newInstance().getClass() + .isAssignableFrom(newState.getClass())) { + isAccepted = true; + break; + } + } catch (ReflectiveOperationException e) { + // Should never happen + logger.warn("{} while creating {} instance: {}", e.getClass().getSimpleName(), + state.getClass().getSimpleName(), e.getMessage()); + } + } + } + if (isAccepted) { + item.setHistoricState(newState, dateTime); + } else { + logger.debug("Received update of a not accepted type ({}) for item {}", + newState.getClass().getSimpleName(), itemName); + } + } catch (ItemNotFoundException e) { + logger.debug("Received update for non-existing item: {}", e.getMessage()); + } + } } 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 2954ca39869..25c0a23d96c 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java @@ -12,6 +12,7 @@ */ package org.openhab.core.items; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -219,6 +220,18 @@ public void setState(State state) { applyState(state); } + /** + * Set a historic state. + * + * Subclasses may override this method in order to do necessary conversions upfront. Afterwards, + * {@link #applyHistoricState(State)} should be called by classes overriding this method. + * + * @param state historic state of this item + */ + public void setHistoricState(State state, ZonedDateTime dateTime) { + applyHistoricState(state, dateTime); + } + /** * Sets new state, notifies listeners and sends events. * @@ -236,6 +249,18 @@ protected final void applyState(State state) { } } + /** + * Sets new state, notifies listeners and sends events. + * + * Classes overriding the {@link #setState(State)} method should call this method in order to actually set the + * state, inform listeners and send the event. + * + * @param state new state of this item + */ + protected final void applyHistoricState(State state, ZonedDateTime dateTime) { + notifyHistoryListeners(state, dateTime); + } + private void sendStateChangedEvent(State newState, State oldState) { if (eventPublisher != null) { eventPublisher.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); @@ -269,6 +294,22 @@ protected void notifyListeners(final State oldState, final State newState) { } } + protected void notifyHistoryListeners(final State state, final ZonedDateTime dateTime) { + // if nothing has changed, we send update notifications + Set clonedListeners = new CopyOnWriteArraySet<>(listeners); + ExecutorService pool = ThreadPoolManager.getPool(ITEM_THREADPOOLNAME); + clonedListeners.forEach(listener -> pool.execute(() -> { + try { + if (listener instanceof HistoricStateChangeListener) { + ((HistoricStateChangeListener) listener).historicStateUpdated(GenericItem.this, state, dateTime); + } + } catch (Exception e) { + logger.warn("failed notifying listener '{}' about historic state of item {}: {}", listener, + GenericItem.this.getName(), e.getMessage(), e); + } + })); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java new file mode 100644 index 00000000000..c63c42b6629 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2022 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; + +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + *

+ * This interface must be implemented by all classes that want to be notified about changes in the state of an item. + * + *

+ * The {@link GenericItem} class provides the possibility to register such listeners. + * + * @author Kai Kreuzer - Initial contribution + */ +@NonNullByDefault +public interface HistoricStateChangeListener extends StateChangeListener { + + /** + * This method is called, if a state was updated, but has not changed + * + * @param item the item whose state was updated + * @param state the current state, same before and after the update + */ + public void historicStateUpdated(Item item, State state, ZonedDateTime dateTime); +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java index 3598ca1cee7..23321b442b8 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java @@ -33,7 +33,8 @@ @NonNullByDefault public abstract class AbstractItemEventSubscriber implements EventSubscriber { - private final Set subscribedEventTypes = Set.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE); + private final Set subscribedEventTypes = Set.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE, + ItemHistoricStateEvent.TYPE); @Override public Set getSubscribedEventTypes() { @@ -51,6 +52,8 @@ public void receive(Event event) { receiveUpdate((ItemStateEvent) event); } else if (event instanceof ItemCommandEvent) { receiveCommand((ItemCommandEvent) event); + } else if (event instanceof ItemHistoricStateEvent) { + receiveHistoricState((ItemHistoricStateEvent) event); } } @@ -73,4 +76,14 @@ protected void receiveUpdate(ItemStateEvent updateEvent) { // Default implementation: do nothing. // Can be implemented by subclass in order to handle item updates. } + + /** + * Callback method for receiving item historic state events from the openHAB event bus. + * + * @param event the item historic state event + */ + protected void receiveHistoricState(ItemHistoricStateEvent event) { + // Default implementation: do nothing. + // Can be implemented by subclass in order to handle item historic states. + } } 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 e7aaf00a6d2..891dfa7e06c 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 @@ -14,6 +14,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.time.ZonedDateTime; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -50,6 +51,8 @@ public class ItemEventFactory extends AbstractEventFactory { private static final String ITEM_STATE_EVENT_TOPIC = "openhab/items/{itemName}/state"; + private static final String ITEM_HISTORIC_STATE_EVENT_TOPIC = "openhab/items/{itemName}/historicstate"; + 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"; @@ -66,9 +69,9 @@ public class ItemEventFactory extends AbstractEventFactory { * Constructs a new ItemEventFactory. */ public ItemEventFactory() { - super(Set.of(ItemCommandEvent.TYPE, ItemStateEvent.TYPE, ItemStatePredictedEvent.TYPE, - ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, ItemRemovedEvent.TYPE, - GroupItemStateChangedEvent.TYPE)); + super(Set.of(ItemCommandEvent.TYPE, ItemHistoricStateEvent.TYPE, ItemStateEvent.TYPE, + ItemStatePredictedEvent.TYPE, ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, + ItemRemovedEvent.TYPE, GroupItemStateChangedEvent.TYPE)); } @Override @@ -78,6 +81,8 @@ protected Event createEventByType(String eventType, String topic, String payload return createCommandEvent(topic, payload, source); } else if (ItemStateEvent.TYPE.equals(eventType)) { return createStateEvent(topic, payload, source); + } else if (ItemHistoricStateEvent.TYPE.equals(eventType)) { + return createHistoricStateEvent(topic, payload, source); } else if (ItemStatePredictedEvent.TYPE.equals(eventType)) { return createStatePredictedEvent(topic, payload); } else if (ItemStateChangedEvent.TYPE.equals(eventType)) { @@ -117,6 +122,14 @@ private Event createStateEvent(String topic, String payload, @Nullable String so return new ItemStateEvent(topic, payload, itemName, state, source); } + private Event createHistoricStateEvent(String topic, String payload, @Nullable String source) { + String itemName = getItemName(topic); + ItemHistoricStateEventPayloadBean bean = deserializePayload(payload, ItemHistoricStateEventPayloadBean.class); + State state = getState(bean.getType(), bean.getValue()); + ZonedDateTime dateTime = ZonedDateTime.parse(bean.getDateTime()); + return new ItemHistoricStateEvent(topic, payload, itemName, state, dateTime, source); + } + private Event createStatePredictedEvent(String topic, String payload) { String itemName = getItemName(topic); ItemStatePredictedEventPayloadBean bean = deserializePayload(payload, ItemStatePredictedEventPayloadBean.class); @@ -268,6 +281,26 @@ public static ItemEvent createStateEvent(String itemName, State state) { return createStateEvent(itemName, state, null); } + /** + * Creates an item historic state event. + * + * @param itemName the name of the item to send the state update for + * @param state the new state to send + * @param source the name of the source identifying the sender (can be null) + * @return the created item state event + * @throws IllegalArgumentException if itemName or state is null + */ + public static ItemHistoricStateEvent createHistoricStateEvent(String itemName, State state, ZonedDateTime dateTime, + @Nullable String source) { + assertValidArguments(itemName, state, "state"); + // TODO: use a different topic for historic state ? Would require changes to ItemUpdater. + String topic = buildTopic(ITEM_STATE_EVENT_TOPIC, itemName); + ItemHistoricStateEventPayloadBean bean = new ItemHistoricStateEventPayloadBean(getStateType(state), + state.toFullString(), dateTime.toString()); + String payload = serializePayload(bean); + return new ItemHistoricStateEvent(topic, payload, itemName, state, dateTime, source); + } + /** * Creates an item state predicted event. * @@ -441,6 +474,40 @@ public String getValue() { } } + /** + * This is a java bean that is used to serialize/deserialize item historic state event payload. + */ + private static class ItemHistoricStateEventPayloadBean { + private @NonNullByDefault({}) String type; + private @NonNullByDefault({}) String value; + private @NonNullByDefault({}) String dateTime; + + /** + * Default constructor for deserialization e.g. by Gson. + */ + @SuppressWarnings("unused") + protected ItemHistoricStateEventPayloadBean() { + } + + public ItemHistoricStateEventPayloadBean(String type, String value, String dateTime) { + this.type = type; + this.value = value; + this.dateTime = dateTime; + } + + public String getType() { + return type; + } + + public String getValue() { + return value; + } + + public String getDateTime() { + return dateTime; + } + } + /** * This is a java bean that is used to serialize/deserialize item state changed event payload. */ diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java new file mode 100644 index 00000000000..f11d2464820 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2022 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 java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +@NonNullByDefault +public class ItemHistoricStateEvent extends ItemEvent { + + private static DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; // ofLocalizedDateTime(FormatStyle.SHORT); + + /** + * The item state changed event type. + */ + public static final String TYPE = ItemHistoricStateEvent.class.getSimpleName(); + + protected final State itemState; + + protected final ZonedDateTime dateTime; + + /** + * 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 ItemHistoricStateEvent(String topic, String payload, String itemName, State itemState, + ZonedDateTime dateTime, @Nullable String source) { + super(topic, payload, itemName, source); + this.itemState = itemState; + this.dateTime = dateTime; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * Gets the item's historic state. + * + * @return the item's historic state + */ + public State getItemState() { + return itemState; + } + + /** + * Gets the date time. + * + * @return the date time + */ + public ZonedDateTime getDateTime() { + return dateTime; + } + + @Override + public String toString() { + return String.format("Item '%s' state at %s set to %s", itemName, + dateTime == null ? "null" : dateTime.format(DATETIME_FORMAT), itemState); + } +} From 91b0235581455689e84f9fc8b8f949ccc0d7c7f1 Mon Sep 17 00:00:00 2001 From: altaroca <8048513+altaroca@users.noreply.github.com> Date: Mon, 20 Jun 2022 19:30:16 +0200 Subject: [PATCH 2/2] address maintainer's comments --- .../internal/PersistenceManagerImpl.java | 19 +++--- .../core/thing/binding/BaseThingHandler.java | 7 +- .../thing/binding/ThingHandlerCallback.java | 32 ++++----- .../core/thing/internal/HistoricState.java | 8 +++ .../core/internal/items/ItemUpdater.java | 65 +++++++------------ .../org/openhab/core/items/GenericItem.java | 5 +- .../items/HistoricStateChangeListener.java | 39 ----------- .../core/items/StateChangeListener.java | 13 ++++ .../core/items/events/ItemEventFactory.java | 3 +- .../items/events/ItemHistoricStateEvent.java | 9 ++- 10 files changed, 87 insertions(+), 113 deletions(-) delete mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java index 22df11a920f..3143b7bc0eb 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java @@ -32,11 +32,11 @@ import org.openhab.core.common.SafeCaller; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; -import org.openhab.core.items.HistoricStateChangeListener; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistryChangeListener; +import org.openhab.core.items.StateChangeListener; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.persistence.ModifiablePersistenceService; @@ -78,7 +78,7 @@ @Component(immediate = true, service = PersistenceManager.class) @NonNullByDefault public class PersistenceManagerImpl - implements ItemRegistryChangeListener, PersistenceManager, HistoricStateChangeListener, ReadyTracker { + implements ItemRegistryChangeListener, PersistenceManager, StateChangeListener, ReadyTracker { private final Logger logger = LoggerFactory.getLogger(PersistenceManagerImpl.class); @@ -164,10 +164,12 @@ private void handleStateEvent(Item item, boolean onlyChanges) { * Calls all persistence services which use change or update policy for the given item * * @param item the item to persist - * @param onlyChanges true, if it has the change strategy, false otherwise + * @param state the state + * @param dateTime the date time when the state is valid */ - private void handleHistoricStateEvent(Item item, State state, ZonedDateTime dateTime, boolean onlyChanges) { - logger.debug("Persisting item '{}' historic state '{}' at {}", item.getName(), state.toString(), dateTime.toString()); + private void handleHistoricStateEvent(Item item, State state, ZonedDateTime dateTime) { + logger.debug("Persisting item '{}' historic state '{}' at {}", item.getName(), state.toString(), + dateTime.toString()); synchronized (persistenceServiceConfigs) { for (Entry entry : persistenceServiceConfigs .entrySet()) { @@ -179,11 +181,10 @@ private void handleHistoricStateEvent(Item item, State state, ZonedDateTime date .get(serviceName); logger.debug(" Using ModifiablePersistenceService '{}'", serviceName); for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, onlyChanges ? PersistenceStrategy.Globals.CHANGE - : PersistenceStrategy.Globals.UPDATE)) { + if (hasStrategy(config, itemConfig, PersistenceStrategy.Globals.UPDATE)) { logger.debug(" trying ItemConfig '{}'", itemConfig.toString()); if (appliesToItem(itemConfig, item)) { - logger.debug(" config applies"); // false ?? + logger.debug(" config applies"); service.store(item, dateTime, state); } } @@ -515,7 +516,7 @@ public void stateUpdated(Item item, State state) { @Override public void historicStateUpdated(Item item, State state, ZonedDateTime dateTime) { - handleHistoricStateEvent(item, state, dateTime, false); + handleHistoricStateEvent(item, state, dateTime); } @Override diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java index e0de9865043..4fe52187b5b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandler.java @@ -293,12 +293,12 @@ protected void updateState(String channelID, State state) { * Updates a historic state of the thing. * * @param channelUID unique id of the channel, which was updated - * @param state new state + * @param state the state + * @param dateTime the date time when the state is valid */ protected void updateHistoricState(ChannelUID channelUID, State state, ZonedDateTime dateTime) { synchronized (this) { if (this.callback != null) { - // TODO: check if this historic state is later than any existing state this.callback.historicStateUpdated(channelUID, state, dateTime); } else { logger.warn( @@ -314,7 +314,8 @@ protected void updateHistoricState(ChannelUID channelUID, State state, ZonedDate * unique channel UID from the given ID. * * @param channelID id of the channel, which was updated - * @param state new state + * @param state the state + * @param dateTime the date time when the state is valid */ protected void updateHistoricState(String channelID, State state, ZonedDateTime dateTime) { ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelID); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java index 5137724c27e..831f51ca111 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/ThingHandlerCallback.java @@ -53,17 +53,17 @@ public interface ThingHandlerCallback { /** * Informs about an updated state for a channel. * - * @param channelUID channel UID (must not be null) - * @param state state (must not be null) + * @param channelUID channel UID + * @param state state */ void stateUpdated(ChannelUID channelUID, State state); /** * Informs about an update to a historic state for a channel. * - * @param channelUID channel UID (must not be null) - * @param state state (must not be null) - * @param dateTime date time (must not be null) + * @param channelUID channel UID + * @param state state + * @param dateTime date time */ void historicStateUpdated(ChannelUID channelUID, State state, ZonedDateTime dateTime); @@ -78,15 +78,15 @@ public interface ThingHandlerCallback { /** * Informs about an updated status of a thing. * - * @param thing thing (must not be null) - * @param thingStatus thing status (must not be null) + * @param thing thing + * @param thingStatus thing status */ void statusUpdated(Thing thing, ThingStatusInfo thingStatus); /** * Informs about an update of the whole thing. * - * @param thing thing that was updated (must not be null) + * @param thing thing that was updated * @throws IllegalStateException if the {@link Thing} is read-only. */ void thingUpdated(Thing thing); @@ -94,7 +94,7 @@ public interface ThingHandlerCallback { /** * Validates the given configuration parameters against the configuration description. * - * @param thing thing with the updated configuration (must not be null) + * @param thing thing with the updated configuration * @param configurationParameters the configuration parameters to be validated * @throws ConfigValidationException if one or more of the given configuration parameters do not match * their declarations in the configuration description @@ -104,7 +104,7 @@ public interface ThingHandlerCallback { /** * Validates the given configuration parameters against the configuration description. * - * @param channel channel with the updated configuration (must not be null) + * @param channel channel with the updated configuration * @param configurationParameters the configuration parameters to be validated * @throws ConfigValidationException if one or more of the given configuration parameters do not match * their declarations in the configuration description @@ -139,8 +139,8 @@ public interface ThingHandlerCallback { /** * Informs the framework that the ThingType of the given {@link Thing} should be changed. * - * @param thing thing that should be migrated to another ThingType (must not be null) - * @param thingTypeUID the new type of the thing (must not be null) + * @param thing thing that should be migrated to another ThingType + * @param thingTypeUID the new type of the thing * @param configuration a configuration that should be applied to the given {@link Thing} */ void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration); @@ -148,7 +148,7 @@ public interface ThingHandlerCallback { /** * Informs the framework that a channel has been triggered. * - * @param thing thing (must not be null) + * @param thing thing * @param channelUID UID of the channel over which has been triggered. * @param event Event. */ @@ -169,7 +169,7 @@ public interface ThingHandlerCallback { * modify it. The methods {@link BaseThingHandler#editThing(Thing)} and {@link BaseThingHandler#updateThing(Thing)} * must be called to persist the changes. * - * @param thing {@link Thing} (must not be null) + * @param thing {@link Thing} * @param channelUID the UID of the {@link Channel} to be edited * @return a preconfigured {@link ChannelBuilder} * @throws IllegalArgumentException if no {@link Channel} with the given UID exists for the given {@link Thing} @@ -191,7 +191,7 @@ List createChannelBuilders(ChannelGroupUID channelGroupUID, /** * Returns whether at least one item is linked for the given UID of the channel. * - * @param channelUID UID of the channel (must not be null) + * @param channelUID UID of the channel * @return true if at least one item is linked, false otherwise */ boolean isChannelLinked(ChannelUID channelUID); @@ -199,7 +199,7 @@ List createChannelBuilders(ChannelGroupUID channelGroupUID, /** * Returns the bridge of the thing. * - * @param bridgeUID {@link ThingUID} UID of the bridge (must not be null) + * @param bridgeUID {@link ThingUID} UID of the bridge * @return returns the bridge of the thing or null if the thing has no bridge */ @Nullable diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java index 60b4786aa24..43e82a4f823 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java @@ -14,9 +14,17 @@ import java.time.ZonedDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; +/** + * A wrapper class for {@link State} which represents an item state plus a date time when the state is valid. + * + * @author Jan M. Hochstein + * + */ +@NonNullByDefault public class HistoricState implements State { private State state; diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java index 1b495912cdc..c988f9f26f9 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java @@ -58,26 +58,7 @@ protected void receiveUpdate(ItemStateEvent updateEvent) { State newState = updateEvent.getItemState(); try { GenericItem item = (GenericItem) itemRegistry.getItem(itemName); - boolean isAccepted = false; - if (item.getAcceptedDataTypes().contains(newState.getClass())) { - isAccepted = true; - } else { - // Look for class hierarchy - for (Class state : item.getAcceptedDataTypes()) { - try { - if (!state.isEnum() && state.getDeclaredConstructor().newInstance().getClass() - .isAssignableFrom(newState.getClass())) { - isAccepted = true; - break; - } - } catch (ReflectiveOperationException e) { - // Should never happen - logger.warn("{} while creating {} instance: {}", e.getClass().getSimpleName(), - state.getClass().getSimpleName(), e.getMessage()); - } - } - } - if (isAccepted) { + if (isStateAcceptableForItem(item, newState)) { item.setState(newState); } else { logger.debug("Received update of a not accepted type ({}) for item {}", @@ -108,26 +89,7 @@ protected void receiveHistoricState(ItemHistoricStateEvent event) { ZonedDateTime dateTime = event.getDateTime(); try { GenericItem item = (GenericItem) itemRegistry.getItem(itemName); - boolean isAccepted = false; - if (item.getAcceptedDataTypes().contains(newState.getClass())) { - isAccepted = true; - } else { - // Look for class hierarchy - for (Class state : item.getAcceptedDataTypes()) { - try { - if (!state.isEnum() && state.getDeclaredConstructor().newInstance().getClass() - .isAssignableFrom(newState.getClass())) { - isAccepted = true; - break; - } - } catch (ReflectiveOperationException e) { - // Should never happen - logger.warn("{} while creating {} instance: {}", e.getClass().getSimpleName(), - state.getClass().getSimpleName(), e.getMessage()); - } - } - } - if (isAccepted) { + if (isStateAcceptableForItem(item, newState)) { item.setHistoricState(newState, dateTime); } else { logger.debug("Received update of a not accepted type ({}) for item {}", @@ -137,4 +99,27 @@ protected void receiveHistoricState(ItemHistoricStateEvent event) { logger.debug("Received update for non-existing item: {}", e.getMessage()); } } + + private boolean isStateAcceptableForItem(GenericItem item, State newState) { + boolean isAccepted = false; + if (item.getAcceptedDataTypes().contains(newState.getClass())) { + isAccepted = true; + } else { + // Look for class hierarchy + for (Class state : item.getAcceptedDataTypes()) { + try { + if (!state.isEnum() && state.getDeclaredConstructor().newInstance().getClass() + .isAssignableFrom(newState.getClass())) { + isAccepted = true; + break; + } + } catch (ReflectiveOperationException e) { + // Should never happen + logger.warn("{} while creating {} instance: {}", e.getClass().getSimpleName(), + state.getClass().getSimpleName(), e.getMessage()); + } + } + } + return isAccepted; + } } 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 25c0a23d96c..e43a4d2d3b7 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 @@ -295,14 +295,11 @@ protected void notifyListeners(final State oldState, final State newState) { } protected void notifyHistoryListeners(final State state, final ZonedDateTime dateTime) { - // if nothing has changed, we send update notifications Set clonedListeners = new CopyOnWriteArraySet<>(listeners); ExecutorService pool = ThreadPoolManager.getPool(ITEM_THREADPOOLNAME); clonedListeners.forEach(listener -> pool.execute(() -> { try { - if (listener instanceof HistoricStateChangeListener) { - ((HistoricStateChangeListener) listener).historicStateUpdated(GenericItem.this, state, dateTime); - } + listener.historicStateUpdated(GenericItem.this, state, dateTime); } catch (Exception e) { logger.warn("failed notifying listener '{}' about historic state of item {}: {}", listener, GenericItem.this.getName(), e.getMessage(), e); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java deleted file mode 100644 index c63c42b6629..00000000000 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/HistoricStateChangeListener.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2010-2022 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; - -import java.time.ZonedDateTime; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.types.State; - -/** - *

- * This interface must be implemented by all classes that want to be notified about changes in the state of an item. - * - *

- * The {@link GenericItem} class provides the possibility to register such listeners. - * - * @author Kai Kreuzer - Initial contribution - */ -@NonNullByDefault -public interface HistoricStateChangeListener extends StateChangeListener { - - /** - * This method is called, if a state was updated, but has not changed - * - * @param item the item whose state was updated - * @param state the current state, same before and after the update - */ - public void historicStateUpdated(Item item, State state, ZonedDateTime dateTime); -} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java index bf9192bf4ac..368f7697f44 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java @@ -12,6 +12,8 @@ */ package org.openhab.core.items; +import java.time.ZonedDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.types.State; @@ -43,4 +45,15 @@ public interface StateChangeListener { * @param state the current state, same before and after the update */ public void stateUpdated(Item item, State state); + + /** + * This method is called, if a historic state was updated + * + * @param item the item whose state was updated + * @param state the state + * @param dateTime the date time when the state is valid + */ + default public void historicStateUpdated(Item item, State state, ZonedDateTime dateTime) { + throw new UnsupportedOperationException("this listener does not know how to handle historic states"); + } } 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 891dfa7e06c..02a3b338f14 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 @@ -286,6 +286,7 @@ public static ItemEvent createStateEvent(String itemName, State state) { * * @param itemName the name of the item to send the state update for * @param state the new state to send + * @param dateTime the date time when the state is valid * @param source the name of the source identifying the sender (can be null) * @return the created item state event * @throws IllegalArgumentException if itemName or state is null @@ -294,7 +295,7 @@ public static ItemHistoricStateEvent createHistoricStateEvent(String itemName, S @Nullable String source) { assertValidArguments(itemName, state, "state"); // TODO: use a different topic for historic state ? Would require changes to ItemUpdater. - String topic = buildTopic(ITEM_STATE_EVENT_TOPIC, itemName); + String topic = buildTopic(ITEM_HISTORIC_STATE_EVENT_TOPIC, itemName); ItemHistoricStateEventPayloadBean bean = new ItemHistoricStateEventPayloadBean(getStateType(state), state.toFullString(), dateTime.toString()); String payload = serializePayload(bean); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java index f11d2464820..c6a167acb75 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java @@ -19,6 +19,12 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; +/** + * {@link ItemHistoricStateEvent}s can be used to deliver item historic states through the openHAB event bus. + * Historic state events must be created with the {@link ItemEventFactory}. + * + * @author Jan M. Hochstein - Initial contribution + */ @NonNullByDefault public class ItemHistoricStateEvent extends ItemEvent { @@ -34,12 +40,13 @@ public class ItemHistoricStateEvent extends ItemEvent { protected final ZonedDateTime dateTime; /** - * Constructs a new item state event. + * Constructs a new item historic state event. * * @param topic the topic * @param payload the payload * @param itemName the item name * @param itemState the item state + * @param dateTime the date time * @param source the source, can be null */ protected ItemHistoricStateEvent(String topic, String payload, String itemName, State itemState,