[inmemory] Initial contribution (#15063)
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>
This commit is contained in:
parent
8a67d0ad94
commit
0b6bdad557
|
@ -1961,6 +1961,11 @@
|
||||||
<artifactId>org.openhab.persistence.influxdb</artifactId>
|
<artifactId>org.openhab.persistence.influxdb</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.persistence.inmemory</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.persistence.jdbc</artifactId>
|
<artifactId>org.openhab.persistence.jdbc</artifactId>
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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>
|
|
@ -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>
|
|
@ -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) {
|
||||||
|
};
|
||||||
|
}
|
|
@ -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>
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -423,6 +423,7 @@
|
||||||
<!-- persistence -->
|
<!-- persistence -->
|
||||||
<module>org.openhab.persistence.dynamodb</module>
|
<module>org.openhab.persistence.dynamodb</module>
|
||||||
<module>org.openhab.persistence.influxdb</module>
|
<module>org.openhab.persistence.influxdb</module>
|
||||||
|
<module>org.openhab.persistence.inmemory</module>
|
||||||
<module>org.openhab.persistence.jdbc</module>
|
<module>org.openhab.persistence.jdbc</module>
|
||||||
<module>org.openhab.persistence.jpa</module>
|
<module>org.openhab.persistence.jpa</module>
|
||||||
<module>org.openhab.persistence.mapdb</module>
|
<module>org.openhab.persistence.mapdb</module>
|
||||||
|
|
Loading…
Reference in New Issue