]> git.basschouten.com Git - openhab-addons.git/commitdiff
[inmemory] Initial contribution (#15063)
authorJ-N-K <github@klug.nrw>
Sat, 24 Jun 2023 09:15:09 +0000 (11:15 +0200)
committerGitHub <noreply@github.com>
Sat, 24 Jun 2023 09:15:09 +0000 (11:15 +0200)
This is the initial contribution of a new volatile persistence service. It does store values in memory only and can especially be used for forecasts or other data where volatile storage is sufficient.

Signed-off-by: Jan N. Klug <github@klug.nrw>
bom/openhab-addons/pom.xml
bundles/org.openhab.persistence.inmemory/NOTICE [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/README.md [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/pom.xml [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/src/main/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceService.java [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.persistence.inmemory/src/test/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceTests.java [new file with mode: 0644]
bundles/pom.xml

index 54802e935c9dba1e6e3b7df85d519f39c2021b52..e19070fd25c57a2b10c2c29f3a3d0361399a32ce 100644 (file)
       <artifactId>org.openhab.persistence.influxdb</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.persistence.inmemory</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.persistence.jdbc</artifactId>
diff --git a/bundles/org.openhab.persistence.inmemory/NOTICE b/bundles/org.openhab.persistence.inmemory/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.persistence.inmemory/README.md b/bundles/org.openhab.persistence.inmemory/README.md
new file mode 100644 (file)
index 0000000..f24ab44
--- /dev/null
@@ -0,0 +1,14 @@
+# InMemory Persistence
+
+The InMemory persistence service provides a volatile storage, i.e. it is cleared on shutdown.
+Because of that the `restoreOnStartup` strategy is not supported for this service.
+
+The main use-case is to store data that is needed during runtime, e.g. temporary storage of forecast data that is retrieved from a binding.
+
+Since all data is stored in memory only, there is no default strategy for this service.
+Unlike other persistence services, you MUST add a configuration, otherwise no data will be persisted.
+To avoid excessive memory usage, it is recommended to persist only a limited number of items and use a strategy that stores only data that is actually needed.
+
+The service has a global configuration option `maxEntries` to limit the number of datapoints per item, the default value is `512`.
+When the number of datapoints is reached and a new value is persisted, the oldest (by timestamp) value will be removed.
+A `maxEntries` value of `0` disables automatic purging.
diff --git a/bundles/org.openhab.persistence.inmemory/pom.xml b/bundles/org.openhab.persistence.inmemory/pom.xml
new file mode 100644 (file)
index 0000000..3df08ea
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.persistence.inmemory</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Persistence Service :: InMemory</name>
+
+</project>
diff --git a/bundles/org.openhab.persistence.inmemory/src/main/feature/feature.xml b/bundles/org.openhab.persistence.inmemory/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..6ec7447
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.persistence.inmemory-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-persistence-inmemory" description="InMemory Persistence" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.persistence.inmemory/${project.version}</bundle>
+       </feature>
+
+</features>
diff --git a/bundles/org.openhab.persistence.inmemory/src/main/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceService.java b/bundles/org.openhab.persistence.inmemory/src/main/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceService.java
new file mode 100644 (file)
index 0000000..cffb2ce
--- /dev/null
@@ -0,0 +1,311 @@
+/**
+ * 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.persistence.inmemory.internal;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigParser;
+import org.openhab.core.config.core.ConfigurableService;
+import org.openhab.core.items.Item;
+import org.openhab.core.persistence.FilterCriteria;
+import org.openhab.core.persistence.HistoricItem;
+import org.openhab.core.persistence.ModifiablePersistenceService;
+import org.openhab.core.persistence.PersistenceItemInfo;
+import org.openhab.core.persistence.PersistenceService;
+import org.openhab.core.persistence.strategy.PersistenceStrategy;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the implementation of the volatile {@link PersistenceService}.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { PersistenceService.class,
+        ModifiablePersistenceService.class }, configurationPid = "org.openhab.inmemory", //
+        property = Constants.SERVICE_PID + "=org.openhab.inmemory")
+@ConfigurableService(category = "persistence", label = "InMemory Persistence Service", description_uri = InMemoryPersistenceService.CONFIG_URI)
+public class InMemoryPersistenceService implements ModifiablePersistenceService {
+
+    private static final String SERVICE_ID = "inmemory";
+    private static final String SERVICE_LABEL = "In Memory";
+
+    protected static final String CONFIG_URI = "persistence:inmemory";
+    private final String MAX_ENTRIES_CONFIG = "maxEntries";
+    private final long MAX_ENTRIES_DEFAULT = 512;
+
+    private final Logger logger = LoggerFactory.getLogger(InMemoryPersistenceService.class);
+
+    private final Map<String, PersistItem> persistMap = new ConcurrentHashMap<>();
+    private long maxEntries = MAX_ENTRIES_DEFAULT;
+
+    @Activate
+    public void activate(Map<String, Object> config) {
+        modified(config);
+        logger.debug("InMemory persistence service is now activated.");
+    }
+
+    @Modified
+    public void modified(Map<String, Object> config) {
+        maxEntries = ConfigParser.valueAsOrElse(config.get(MAX_ENTRIES_CONFIG), Long.class, MAX_ENTRIES_DEFAULT);
+
+        persistMap.values().forEach(persistItem -> {
+            Lock lock = persistItem.lock();
+            lock.lock();
+            try {
+                while (persistItem.database().size() > maxEntries) {
+                    persistItem.database().pollFirst();
+                }
+            } finally {
+                lock.unlock();
+            }
+        });
+    }
+
+    @Deactivate
+    public void deactivate() {
+        logger.debug("InMemory persistence service deactivated.");
+    }
+
+    @Override
+    public String getId() {
+        return SERVICE_ID;
+    }
+
+    @Override
+    public String getLabel(@Nullable Locale locale) {
+        return SERVICE_LABEL;
+    }
+
+    @Override
+    public Set<PersistenceItemInfo> getItemInfo() {
+        return persistMap.entrySet().stream().map(this::toItemInfo).collect(Collectors.toSet());
+    }
+
+    @Override
+    public void store(Item item) {
+        internalStore(item.getName(), ZonedDateTime.now(), item.getState());
+    }
+
+    @Override
+    public void store(Item item, @Nullable String alias) {
+        String finalName = Objects.requireNonNullElse(alias, item.getName());
+        internalStore(finalName, ZonedDateTime.now(), item.getState());
+    }
+
+    @Override
+    public void store(Item item, ZonedDateTime date, State state) {
+        internalStore(item.getName(), date, state);
+    }
+
+    @Override
+    public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
+        String itemName = filter.getItemName();
+        if (itemName == null) {
+            return false;
+        }
+
+        PersistItem persistItem = persistMap.get(itemName);
+        if (persistItem == null) {
+            return false;
+        }
+
+        Lock lock = persistItem.lock();
+        lock.lock();
+        try {
+            List<PersistEntry> toRemove = persistItem.database().stream().filter(e -> applies(e, filter)).toList();
+            toRemove.forEach(persistItem.database()::remove);
+        } finally {
+            lock.unlock();
+        }
+        return true;
+    }
+
+    @Override
+    public Iterable<HistoricItem> query(FilterCriteria filter) {
+        String itemName = filter.getItemName();
+        if (itemName == null) {
+            return List.of();
+        }
+
+        PersistItem persistItem = persistMap.get(itemName);
+        if (persistItem == null) {
+            return List.of();
+        }
+
+        Lock lock = persistItem.lock();
+        lock.lock();
+        try {
+            return persistItem.database().stream().filter(e -> applies(e, filter)).map(e -> toHistoricItem(itemName, e))
+                    .toList();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public List<PersistenceStrategy> getDefaultStrategies() {
+        // persist nothing by default
+        return List.of();
+    }
+
+    private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) {
+        Lock lock = itemEntry.getValue().lock();
+        lock.lock();
+        try {
+            String name = itemEntry.getKey();
+            Integer count = itemEntry.getValue().database().size();
+            Instant earliest = itemEntry.getValue().database().first().timestamp().toInstant();
+            Instant latest = itemEntry.getValue().database.last().timestamp.toInstant();
+            return new PersistenceItemInfo() {
+
+                @Override
+                public String getName() {
+                    return name;
+                }
+
+                @Override
+                public @Nullable Integer getCount() {
+                    return count;
+                }
+
+                @Override
+                public @Nullable Date getEarliest() {
+                    return Date.from(earliest);
+                }
+
+                @Override
+                public @Nullable Date getLatest() {
+                    return Date.from(latest);
+                }
+            };
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private HistoricItem toHistoricItem(String itemName, PersistEntry entry) {
+        return new HistoricItem() {
+            @Override
+            public ZonedDateTime getTimestamp() {
+                return entry.timestamp();
+            }
+
+            @Override
+            public State getState() {
+                return entry.state();
+            }
+
+            @Override
+            public String getName() {
+                return itemName;
+            }
+        };
+    }
+
+    private void internalStore(String itemName, ZonedDateTime timestamp, State state) {
+        if (state instanceof UnDefType) {
+            return;
+        }
+
+        PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName,
+                k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)),
+                        new ReentrantLock())));
+
+        Lock lock = persistItem.lock();
+        lock.lock();
+        try {
+            persistItem.database().add(new PersistEntry(timestamp, state));
+
+            while (persistItem.database.size() > maxEntries) {
+                persistItem.database().pollFirst();
+            }
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @SuppressWarnings({ "rawType", "unchecked" })
+    private boolean applies(PersistEntry entry, FilterCriteria filter) {
+        ZonedDateTime beginDate = filter.getBeginDate();
+        if (beginDate != null && entry.timestamp().isBefore(beginDate)) {
+            return false;
+        }
+        ZonedDateTime endDate = filter.getEndDate();
+        if (endDate != null && entry.timestamp().isAfter(endDate)) {
+            return false;
+        }
+
+        State refState = filter.getState();
+        FilterCriteria.Operator operator = filter.getOperator();
+        if (refState == null) {
+            // no state filter
+            return true;
+        }
+
+        if (operator == FilterCriteria.Operator.EQ) {
+            return entry.state().equals(refState);
+        }
+
+        if (operator == FilterCriteria.Operator.NEQ) {
+            return !entry.state().equals(refState);
+        }
+
+        if (entry.state() instanceof Comparable comparableState && entry.state.getClass().equals(refState.getClass())) {
+            if (operator == FilterCriteria.Operator.GT) {
+                return comparableState.compareTo(refState) > 0;
+            }
+            if (operator == FilterCriteria.Operator.GTE) {
+                return comparableState.compareTo(refState) >= 0;
+            }
+            if (operator == FilterCriteria.Operator.LT) {
+                return comparableState.compareTo(refState) < 0;
+            }
+            if (operator == FilterCriteria.Operator.LTE) {
+                return comparableState.compareTo(refState) <= 0;
+            }
+        } else {
+            logger.warn("Using operator {} but state {} is not comparable!", operator, refState);
+        }
+        return true;
+    }
+
+    private record PersistEntry(ZonedDateTime timestamp, State state) {
+    };
+
+    private record PersistItem(TreeSet<PersistEntry> database, Lock lock) {
+    };
+}
diff --git a/bundles/org.openhab.persistence.inmemory/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.persistence.inmemory/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644 (file)
index 0000000..a870359
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="inmemory" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>persistence</type>
+       <name>InMemory Persistence</name>
+       <description>A volatile persistence service to temporarily store data.</description>
+       <connection>none</connection>
+
+       <service-id>org.openhab.inmemory</service-id>
+
+       <config-description>
+               <parameter name="maxEntries" type="integer" min="0">
+                       <label>Maximum Entries</label>
+                       <description>The maximum number of values stored for each item (0 = infinite).</description>
+                       <default>512</default>
+               </parameter>
+       </config-description>
+
+</addon:addon>
diff --git a/bundles/org.openhab.persistence.inmemory/src/test/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceTests.java b/bundles/org.openhab.persistence.inmemory/src/test/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceTests.java
new file mode 100644 (file)
index 0000000..750e7f9
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * 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.persistence.inmemory.internal;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.closeTo;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.when;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+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.items.GenericItem;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.persistence.FilterCriteria;
+import org.openhab.core.persistence.HistoricItem;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link InMemoryPersistenceTests} contains tests for the {@link InMemoryPersistenceService}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class InMemoryPersistenceTests {
+    private static final String ITEM_NAME = "testItem";
+    private static final String ALIAS = "alias";
+
+    private @NonNullByDefault({}) InMemoryPersistenceService service;
+    private @NonNullByDefault({}) @Mock GenericItem item;
+
+    private @NonNullByDefault({}) FilterCriteria filterCriteria;
+
+    @BeforeEach
+    public void setup() {
+        when(item.getName()).thenReturn(ITEM_NAME);
+
+        filterCriteria = new FilterCriteria();
+        filterCriteria.setItemName(ITEM_NAME);
+
+        service = new InMemoryPersistenceService();
+    }
+
+    @Test
+    public void storeDirect() {
+        State state = new DecimalType(1);
+        when(item.getState()).thenReturn(state);
+
+        ZonedDateTime expectedTime = ZonedDateTime.now();
+        service.store(item);
+
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates, hasSize(1));
+        assertThat(storedStates.first().getName(), is(ITEM_NAME));
+        assertThat(storedStates.first().getState(), is(state));
+        assertThat((double) storedStates.first().getTimestamp().toEpochSecond(),
+                is(closeTo(expectedTime.toEpochSecond(), 2)));
+    }
+
+    @Test
+    public void storeAlias() {
+        State state = new PercentType(1);
+        when(item.getState()).thenReturn(state);
+
+        ZonedDateTime expectedTime = ZonedDateTime.now();
+        service.store(item, ALIAS);
+
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+
+        // query with item name should return nothing
+        service.query(filterCriteria).forEach(storedStates::add);
+        assertThat(storedStates, is(empty()));
+
+        filterCriteria.setItemName(ALIAS);
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates.size(), is(1));
+        assertThat(storedStates.first().getName(), is(ALIAS));
+        assertThat(storedStates.first().getState(), is(state));
+        assertThat((double) storedStates.first().getTimestamp().toEpochSecond(),
+                is(closeTo(expectedTime.toEpochSecond(), 2)));
+    }
+
+    @Test
+    public void storeHistoric() {
+        State state = new HSBType("120,100,100");
+        when(item.getState()).thenReturn(state);
+
+        State historicState = new HSBType("40,50,50");
+        ZonedDateTime expectedTime = ZonedDateTime.of(2022, 05, 31, 10, 0, 0, 0, ZoneId.systemDefault());
+        service.store(item, expectedTime, historicState);
+
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates, hasSize(1));
+        assertThat(storedStates.first().getName(), is(ITEM_NAME));
+        assertThat(storedStates.first().getState(), is(historicState));
+        assertThat(storedStates.first().getTimestamp(), is(expectedTime));
+    }
+
+    @Test
+    public void queryWithoutItemNameReturnsEmptyList() {
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+        service.query(new FilterCriteria()).forEach(storedStates::add);
+
+        assertThat(storedStates, is(empty()));
+    }
+
+    @Test
+    public void queryUnknownItemReturnsEmptyList() {
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates, is(empty()));
+    }
+
+    @Test
+    public void removeBetweenTimes() {
+        State historicState1 = new StringType("value1");
+        State historicState2 = new StringType("value2");
+        State historicState3 = new StringType("value3");
+
+        ZonedDateTime expectedTime = ZonedDateTime.of(2022, 05, 31, 10, 0, 0, 0, ZoneId.systemDefault());
+        service.store(item, expectedTime, historicState1);
+        service.store(item, expectedTime.plusHours(2), historicState2);
+        service.store(item, expectedTime.plusHours(4), historicState3);
+
+        // ensure both are stored
+        TreeSet<HistoricItem> storedStates = new TreeSet<>(Comparator.comparing(HistoricItem::getTimestamp));
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates, hasSize(3));
+
+        filterCriteria.setBeginDate(expectedTime.plusHours(1));
+        filterCriteria.setEndDate(expectedTime.plusHours(3));
+        service.remove(filterCriteria);
+
+        filterCriteria = new FilterCriteria();
+        filterCriteria.setItemName(ITEM_NAME);
+        storedStates.clear();
+        service.query(filterCriteria).forEach(storedStates::add);
+
+        assertThat(storedStates, hasSize(2));
+
+        assertThat(storedStates.first().getName(), is(ITEM_NAME));
+        assertThat(storedStates.first().getState(), is(historicState1));
+        assertThat(storedStates.first().getTimestamp(), is(expectedTime));
+
+        assertThat(storedStates.last().getName(), is(ITEM_NAME));
+        assertThat(storedStates.last().getState(), is(historicState3));
+        assertThat(storedStates.last().getTimestamp(), is(expectedTime.plusHours(4)));
+    }
+}
index 4020bcde0491abfe0b5b8a87ac2396715c979965..5fb1ccee88b301b882a5c874e13e7e35452fec79 100644 (file)
     <!-- persistence -->
     <module>org.openhab.persistence.dynamodb</module>
     <module>org.openhab.persistence.influxdb</module>
+    <module>org.openhab.persistence.inmemory</module>
     <module>org.openhab.persistence.jdbc</module>
     <module>org.openhab.persistence.jpa</module>
     <module>org.openhab.persistence.mapdb</module>