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..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 @@ -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; @@ -38,6 +39,7 @@ 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; @@ -158,6 +160,40 @@ 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 state the state + * @param dateTime the date time when the state is valid + */ + 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()) { + 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, PersistenceStrategy.Globals.UPDATE)) { + logger.debug(" trying ItemConfig '{}'", itemConfig.toString()); + if (appliesToItem(itemConfig, item)) { + logger.debug(" config applies"); + service.store(item, dateTime, state); + } + } + } + } + } + } + } + /** * Checks if a given persistence configuration entry has a certain strategy for the given service * @@ -478,6 +514,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); + } + @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..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 @@ -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,40 @@ 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 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) { + 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 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); + 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..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 @@ -12,6 +12,7 @@ */ package org.openhab.core.thing.binding; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -52,11 +53,20 @@ 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 + * @param state state + * @param dateTime date time + */ + void historicStateUpdated(ChannelUID channelUID, State state, ZonedDateTime dateTime); + /** * Informs about a command, which is sent from the channel. * @@ -68,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); @@ -84,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 @@ -94,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 @@ -129,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); @@ -138,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. */ @@ -159,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} @@ -181,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); @@ -189,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/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..43e82a4f823 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/HistoricState.java @@ -0,0 +1,60 @@ +/** + * 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.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; + 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..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 @@ -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; @@ -55,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 {}", @@ -97,4 +81,45 @@ 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); + if (isStateAcceptableForItem(item, newState)) { + 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()); + } + } + + 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 2954ca39869..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 @@ -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,19 @@ protected void notifyListeners(final State oldState, final State newState) { } } + protected void notifyHistoryListeners(final State state, final ZonedDateTime dateTime) { + Set clonedListeners = new CopyOnWriteArraySet<>(listeners); + ExecutorService pool = ThreadPoolManager.getPool(ITEM_THREADPOOLNAME); + clonedListeners.forEach(listener -> pool.execute(() -> { + try { + 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/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/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..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 @@ -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,27 @@ 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 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 + */ + 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_HISTORIC_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 +475,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..c6a167acb75 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemHistoricStateEvent.java @@ -0,0 +1,87 @@ +/** + * 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; + +/** + * {@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 { + + 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 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, + 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); + } +}