Persistence aliases (#4363)

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege
2025-02-18 18:30:49 +01:00
committed by GitHub
parent 3458e9fba3
commit fd53c4cb1d
17 changed files with 290 additions and 73 deletions
@@ -16,9 +16,13 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Consumes;
@@ -104,6 +108,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* @author Lyubomir Papazov - Change java.util.Date references to be of type java.time.ZonedDateTime
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
* @author Wouter Born - Migrated to OpenAPI annotations
* @author Mark Herwege - Implement aliases
*/
@Component
@JaxrsResource
@@ -181,9 +186,10 @@ public class PersistenceResource implements RESTResource {
PersistenceService service = persistenceServiceRegistry.get(serviceId);
if (service != null) {
List<PersistenceStrategy> strategies = service.getDefaultStrategies();
List<PersistenceItemConfiguration> configs = List.of(
new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null));
configuration = new PersistenceServiceConfiguration(serviceId, configs, strategies, strategies,
List<PersistenceItemConfiguration> configs = List
.of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), strategies, null));
Map<String, String> aliases = Map.of();
configuration = new PersistenceServiceConfiguration(serviceId, configs, aliases, strategies, strategies,
List.of());
editable = true;
}
@@ -368,6 +374,9 @@ public class PersistenceResource implements RESTResource {
// If serviceId is null, then use the default service
PersistenceService service;
String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId();
if (effectiveServiceId == null) {
return null;
}
service = persistenceServiceRegistry.get(effectiveServiceId);
if (service == null) {
@@ -416,6 +425,8 @@ public class PersistenceResource implements RESTResource {
ItemHistoryDTO dto = new ItemHistoryDTO();
dto.name = itemName;
PersistenceServiceConfiguration config = persistenceServiceConfigurationRegistry.get(effectiveServiceId);
String alias = config != null ? config.getAliases().get(itemName) : null;
// If "boundary" is true then we want to get one value before and after the requested period
// This is necessary for values that don't change often otherwise data will start after the start of the graph
@@ -427,7 +438,7 @@ public class PersistenceResource implements RESTResource {
filterBeforeStart.setEndDate(dateTimeBegin);
filterBeforeStart.setPageSize(1);
filterBeforeStart.setOrdering(Ordering.DESCENDING);
result = qService.query(filterBeforeStart);
result = qService.query(filterBeforeStart, alias);
if (result.iterator().hasNext()) {
dto.addData(dateTimeBegin.toInstant().toEpochMilli(), result.iterator().next().getState());
quantity++;
@@ -446,7 +457,7 @@ public class PersistenceResource implements RESTResource {
filter.setBeginDate(dateTimeBegin);
filter.setEndDate(dateTimeEnd);
filter.setOrdering(Ordering.ASCENDING);
result = qService.query(filter);
result = qService.query(filter, alias);
Iterator<HistoricItem> it = result.iterator();
// Iterate through the data
@@ -478,7 +489,7 @@ public class PersistenceResource implements RESTResource {
filterAfterEnd.setBeginDate(dateTimeEnd);
filterAfterEnd.setPageSize(1);
filterAfterEnd.setOrdering(Ordering.ASCENDING);
result = qService.query(filterAfterEnd);
result = qService.query(filterAfterEnd, alias);
if (result.iterator().hasNext()) {
dto.addData(dateTimeEnd.toInstant().toEpochMilli(), result.iterator().next().getState());
quantity++;
@@ -544,26 +555,60 @@ public class PersistenceResource implements RESTResource {
private Response getServiceItemList(@Nullable String serviceId) {
// If serviceId is null, then use the default service
PersistenceService service;
if (serviceId == null) {
service = persistenceServiceRegistry.getDefault();
} else {
service = persistenceServiceRegistry.get(serviceId);
String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId();
if (effectiveServiceId == null) {
logger.debug("Persistence service not found '{}'.", effectiveServiceId);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
"Persistence service not found: " + effectiveServiceId);
}
service = persistenceServiceRegistry.get(effectiveServiceId);
if (service == null) {
logger.debug("Persistence service not found '{}'.", serviceId);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not found: " + serviceId);
logger.debug("Persistence service not found '{}'.", effectiveServiceId);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
"Persistence service not found: " + effectiveServiceId);
}
if (!(service instanceof QueryablePersistenceService)) {
logger.debug("Persistence service not queryable '{}'.", serviceId);
logger.debug("Persistence service not queryable '{}'.", effectiveServiceId);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
"Persistence service not queryable: " + serviceId);
"Persistence service not queryable: " + effectiveServiceId);
}
QueryablePersistenceService qService = (QueryablePersistenceService) service;
return JSONResponse.createResponse(Status.OK, qService.getItemInfo(), "");
PersistenceServiceConfiguration config = persistenceServiceConfigurationRegistry.get(effectiveServiceId);
Map<String, String> aliases = config != null ? config.getAliases() : Map.of();
Set<PersistenceItemInfo> itemInfo = qService.getItemInfo().stream().map(info -> {
String alias = aliases.get(info.getName());
if (alias != null) {
return new PersistenceItemInfo() {
@Override
public String getName() {
return alias;
}
@Override
public @Nullable Integer getCount() {
return info.getCount();
}
@Override
public @Nullable Date getEarliest() {
return info.getEarliest();
}
@Override
public @Nullable Date getLatest() {
return info.getLatest();
}
};
} else {
return info;
}
}).collect(Collectors.toSet());
return JSONResponse.createResponse(Status.OK, itemInfo, "");
}
private Response deletePersistenceItemData(@Nullable String serviceId, String itemName, @Nullable String timeBegin,
@@ -601,13 +646,15 @@ public class PersistenceResource implements RESTResource {
// This is necessary for values that don't change often otherwise data will start after the start of the graph
// (or not at all if there's no change during the graph period)
FilterCriteria filter = new FilterCriteria();
PersistenceServiceConfiguration config = persistenceServiceConfigurationRegistry.get(serviceId);
String alias = config != null ? config.getAliases().get(itemName) : null;
filter.setItemName(itemName);
filter.setBeginDate(dateTimeBegin);
filter.setEndDate(dateTimeEnd);
ModifiablePersistenceService mService = (ModifiablePersistenceService) service;
try {
mService.remove(filter);
mService.remove(filter, alias);
} catch (IllegalArgumentException e) {
return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Invalid filter parameters.");
}
@@ -620,7 +667,7 @@ public class PersistenceResource implements RESTResource {
String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId();
PersistenceService service = persistenceServiceRegistry.get(effectiveServiceId);
if (service == null) {
if (effectiveServiceId == null || service == null) {
logger.warn("Persistence service not found '{}'.", effectiveServiceId);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
"Persistence service not found: " + effectiveServiceId);
@@ -658,7 +705,9 @@ public class PersistenceResource implements RESTResource {
}
ModifiablePersistenceService mService = (ModifiablePersistenceService) service;
mService.store(item, dateTime, state);
PersistenceServiceConfiguration config = persistenceServiceConfigurationRegistry.get(effectiveServiceId);
String alias = config != null ? config.getAliases().get(itemName) : null;
mService.store(item, dateTime, state, alias);
persistenceManager.handleExternalPersistenceDataChange(mService, item);
@@ -17,7 +17,7 @@ import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.time.ZoneId;
@@ -58,6 +58,7 @@ import org.openhab.core.types.UnDefType;
* Tests for PersistenceItem Restresource
*
* @author Stefan Triller - Initial contribution
* @author Mark Herwege - Implement aliases
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -112,7 +113,7 @@ public class PersistenceResourceTest {
});
}
when(pServiceMock.query(any())).thenReturn(items);
when(pServiceMock.query(any(), any())).thenReturn(items);
when(persistenceServiceRegistryMock.get(PERSISTENCE_SERVICE_ID)).thenReturn(pServiceMock);
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.systemDefault());
@@ -11,6 +11,7 @@ PersistenceModel:
'}'
('Filters' '{' filters+=Filter* '}')?
('Items' '{' configs+=PersistenceConfiguration* '}')?
('Aliases' '{' aliases+=AliasConfiguration* '}')?
;
Strategy:
@@ -57,7 +58,7 @@ NotIncludeFilter:
PersistenceConfiguration:
items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig)
(',' items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig))* ('->' alias=STRING)?
(',' items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig))*
((':' ('strategy' '=' strategies+=[Strategy|ID] (',' strategies+=[Strategy|ID])*)?
('filter' '=' filters+=[Filter|ID] (',' filters+=[Filter|ID])*)?)
| ';')
@@ -84,6 +85,10 @@ GroupExcludeConfig:
'!' groupExclude=ID '*'
;
AliasConfiguration:
item=ID '->' alias=(ID|STRING)
;
DECIMAL returns ecore::EBigDecimal :
'-'? INT ('.' INT)?
;
@@ -13,6 +13,7 @@
package org.openhab.core.model.persistence.internal;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -24,6 +25,7 @@ import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.model.core.EventType;
import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.core.ModelRepositoryChangeListener;
import org.openhab.core.model.persistence.persistence.AliasConfiguration;
import org.openhab.core.model.persistence.persistence.AllConfig;
import org.openhab.core.model.persistence.persistence.CronStrategy;
import org.openhab.core.model.persistence.persistence.EqualsFilter;
@@ -71,6 +73,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Move non-model logic to core.persistence
* @author Jan N. Klug - Refactored to {@link PersistenceServiceConfigurationProvider}
* @author Mark Herwege - Separate alias handling
*/
@Component(immediate = true, service = PersistenceServiceConfigurationProvider.class)
@NonNullByDefault
@@ -112,8 +115,9 @@ public class PersistenceModelManager extends AbstractProvider<PersistenceService
if (model != null) {
PersistenceServiceConfiguration newConfiguration = new PersistenceServiceConfiguration(serviceName,
mapConfigs(model.getConfigs()), mapStrategies(model.getDefaults()),
mapStrategies(model.getStrategies()), mapFilters(model.getFilters()));
mapConfigs(model.getConfigs()), mapAliases(model.getAliases()),
mapStrategies(model.getDefaults()), mapStrategies(model.getStrategies()),
mapFilters(model.getFilters()));
PersistenceServiceConfiguration oldConfiguration = configurations.put(serviceName,
newConfiguration);
if (oldConfiguration == null) {
@@ -167,10 +171,18 @@ public class PersistenceModelManager extends AbstractProvider<PersistenceService
items.add(new PersistenceItemExcludeConfig(itemExcludeConfig.getItemExclude()));
}
}
return new PersistenceItemConfiguration(items, config.getAlias(), mapStrategies(config.getStrategies()),
return new PersistenceItemConfiguration(items, mapStrategies(config.getStrategies()),
mapFilters(config.getFilters()));
}
private Map<String, String> mapAliases(List<AliasConfiguration> aliases) {
final Map<String, String> map = new HashMap<>();
for (final AliasConfiguration alias : aliases) {
map.put(alias.getItem(), alias.getAlias());
}
return map;
}
private List<PersistenceStrategy> mapStrategies(List<Strategy> strategies) {
final List<PersistenceStrategy> lst = new LinkedList<>();
for (final Strategy strategy : strategies) {
@@ -35,8 +35,8 @@ import org.openhab.core.types.State;
* a filter.
*
* @author Kai Kreuzer - Initial contribution
* @author Lyubomir Papazov - Deprecate methods using java.util and add methods
* that use Java8's ZonedDateTime
* @author Lyubomir Papazov - Deprecate methods using java.util and add methods that use Java8's ZonedDateTime
* @author Mark Herwege - Copy constructor
*/
@NonNullByDefault
public class FilterCriteria {
@@ -91,6 +91,20 @@ public class FilterCriteria {
/** Filter result to only contain entries that evaluate to true with the given operator and state */
private @Nullable State state;
public FilterCriteria() {
}
public FilterCriteria(FilterCriteria filter) {
this.itemName = filter.itemName;
this.beginDate = filter.beginDate;
this.endDate = filter.endDate;
this.pageNumber = filter.pageNumber;
this.pageSize = filter.pageSize;
this.operator = filter.operator;
this.ordering = filter.ordering;
this.state = filter.state;
}
public @Nullable String getItemName() {
return itemName;
}
@@ -25,6 +25,7 @@ import org.openhab.core.types.State;
* and then periodically provide it to the server to be accommodated.
*
* @author Chris Jackson - Initial contribution
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
public interface ModifiablePersistenceService extends QueryablePersistenceService {
@@ -70,10 +71,35 @@ public interface ModifiablePersistenceService extends QueryablePersistenceServic
* Removes data associated with an item from a persistence service.
* If all data is removed for the specified item, the persistence service should free any resources associated with
* the item (e.g. remove any tables or delete files from the storage).
* If the persistence service implementing this method supports aliases for item names, the default implementation
* of {@link #remove(FilterCriteria, String)} should be overriden as well.
*
* @param filter the filter to apply to the data removal. ItemName can not be null.
* @return true if the query executed successfully
* @throws IllegalArgumentException if item name is null.
*/
boolean remove(FilterCriteria filter) throws IllegalArgumentException;
/**
* Removes data associated with an item from a persistence service.
* If all data is removed for the specified item, the persistence service should free any resources associated with
* the item (e.g. remove any tables or delete files from the storage).
* Persistence services supporting aliases, and relying on lookups in the item registry, should override the default
* implementation from this interface.
*
* @param filter the filter to apply to the data removal. ItemName can not be null.
* @param alias for item name in database
* @return true if the query executed successfully
* @throws IllegalArgumentException if item name is null.
*/
default boolean remove(FilterCriteria filter, @Nullable String alias) throws IllegalArgumentException {
// Default implementation changes the filter to have the alias as itemName.
// This gives correct results as long as the persistence service does not rely on a lookup in the item registry
// (in which case the item will not be found).
if (alias != null) {
FilterCriteria aliasFilter = new FilterCriteria(filter).setItemName(alias);
return remove(aliasFilter);
}
return remove(filter);
}
}
@@ -25,15 +25,15 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* This class holds the configuration of a persistence strategy for specific items.
*
* @author Markus Rathgeb - Initial contribution
* @author Mark Herwege - Extract alias configuration
*/
@NonNullByDefault
public record PersistenceItemConfiguration(List<PersistenceConfig> items, @Nullable String alias,
List<PersistenceStrategy> strategies, List<PersistenceFilter> filters) {
public record PersistenceItemConfiguration(List<PersistenceConfig> items, List<PersistenceStrategy> strategies,
List<PersistenceFilter> filters) {
public PersistenceItemConfiguration(final List<PersistenceConfig> items, @Nullable final String alias,
public PersistenceItemConfiguration(final List<PersistenceConfig> items,
@Nullable final List<PersistenceStrategy> strategies, @Nullable final List<PersistenceFilter> filters) {
this.items = items;
this.alias = alias;
this.strategies = Objects.requireNonNullElse(strategies, List.of());
this.filters = Objects.requireNonNullElse(filters, List.of());
}
@@ -15,6 +15,8 @@ package org.openhab.core.persistence;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -29,23 +31,68 @@ import org.openhab.core.types.UnDefType;
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Added getItems method
* @author Mark Herwege - Added methods to retrieve lastUpdate, lastChange and lastState from persistence
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
public interface QueryablePersistenceService extends PersistenceService {
/**
* Queries the {@link PersistenceService} for historic data with a given {@link FilterCriteria}.
* If the persistence service implementing this class supports using aliases for item names, the default
* implementation of {@link #query(FilterCriteria, String)} should be overridden as well.
*
* @param filter the filter to apply to the query
* @return a time series of items
*/
Iterable<HistoricItem> query(FilterCriteria filter);
/**
* Queries the {@link PersistenceService} for historic data with a given {@link FilterCriteria}.
* If the persistence service implementing this interface supports aliases and relies on item registry lookups, the
* default implementation should be overridden to query the database with the aliased name.
*
* @param filter the filter to apply to the query
* @param alias for item name in database
* @return a time series of items
*/
default Iterable<HistoricItem> query(FilterCriteria filter, @Nullable String alias) {
// Default implementation changes the filter to have the alias as itemName and sets it back in the returned
// result.
// This gives correct results as long as the persistence service does not rely on a lookup in the item registry
// (in which case the item will not be found).
String itemName = filter.getItemName();
if (itemName != null && alias != null) {
FilterCriteria aliasFilter = new FilterCriteria(filter).setItemName(alias);
return StreamSupport.stream(query(aliasFilter).spliterator(), false).map(hi -> new HistoricItem() {
@Override
public ZonedDateTime getTimestamp() {
return hi.getTimestamp();
}
@Override
public State getState() {
return hi.getState();
}
@Override
public String getName() {
return itemName;
}
}).collect(Collectors.toList());
}
return query(filter);
}
/**
* Returns a set of {@link PersistenceItemInfo} about items that are stored in the persistence service. This allows
* the persistence service to return information about items that are no long available as an
* {@link org.openhab.core.items.Item} in
* openHAB. If it is not possible to retrieve the information an empty set should be returned.
* {@link org.openhab.core.items.Item} in openHAB. If it is not possible to retrieve the information an empty set
* should be returned.
*
* Note that implementations for method callers that this method may return an alias for an existing item if the
* database does not store the mapping between item name and alias or the reverse mapping is not implemented in the
* persistence service.
*
* @return a set of information about the persisted items
*/
@@ -58,9 +105,12 @@ public interface QueryablePersistenceService extends PersistenceService {
* persisted state. Persistence services can override this default implementation with a more specific or efficient
* algorithm.
*
* @param itemName name of item
* @param alias alias of item
*
* @return a {@link PersistedItem} or null if the item has not been persisted
*/
default @Nullable PersistedItem persistedItem(String itemName) {
default @Nullable PersistedItem persistedItem(String itemName, @Nullable String alias) {
State currentState = UnDefType.NULL;
State previousState = null;
ZonedDateTime lastUpdate = null;
@@ -69,7 +119,7 @@ public interface QueryablePersistenceService extends PersistenceService {
int pageNumber = 0;
FilterCriteria filter = new FilterCriteria().setItemName(itemName).setEndDate(ZonedDateTime.now())
.setOrdering(Ordering.DESCENDING).setPageSize(1000).setPageNumber(pageNumber);
Iterable<HistoricItem> items = query(filter);
Iterable<HistoricItem> items = query(filter, alias);
while (items != null) {
Iterator<HistoricItem> it = items.iterator();
int itemCount = 0;
@@ -16,7 +16,6 @@ import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO} is used for transferring persistence
@@ -29,5 +28,4 @@ public class PersistenceItemConfigurationDTO {
public Collection<String> items = List.of();
public Collection<String> strategies = List.of();
public Collection<String> filters = List.of();
public @Nullable String alias;
}
@@ -14,6 +14,7 @@ package org.openhab.core.persistence.dto;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -26,6 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class PersistenceServiceConfigurationDTO {
public String serviceId = "";
public Collection<PersistenceItemConfigurationDTO> configs = List.of();
public Map<String, String> aliases = Map.of();
public Collection<String> defaults = List.of();
public Collection<PersistenceCronStrategyDTO> cronStrategies = List.of();
public Collection<PersistenceFilterDTO> thresholdFilters = List.of();
@@ -41,6 +41,8 @@ import org.openhab.core.persistence.ModifiablePersistenceService;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.PersistenceServiceRegistry;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.registry.PersistenceServiceConfiguration;
import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry;
import org.openhab.core.types.State;
import org.openhab.core.types.TimeSeries;
import org.openhab.core.types.TypeParser;
@@ -73,12 +75,15 @@ import org.slf4j.LoggerFactory;
public class PersistenceExtensions {
private static @Nullable PersistenceServiceRegistry registry;
private static @Nullable PersistenceServiceConfigurationRegistry configRegistry;
private static @Nullable TimeZoneProvider timeZoneProvider;
@Activate
public PersistenceExtensions(@Reference PersistenceServiceRegistry registry,
@Reference PersistenceServiceConfigurationRegistry configRegistry,
@Reference TimeZoneProvider timeZoneProvider) {
PersistenceExtensions.registry = registry;
PersistenceExtensions.configRegistry = configRegistry;
PersistenceExtensions.timeZoneProvider = timeZoneProvider;
}
@@ -109,7 +114,7 @@ public class PersistenceExtensions {
}
PersistenceService service = getService(effectiveServiceId);
if (service != null) {
service.store(item);
service.store(item, getAlias(item, effectiveServiceId));
return;
}
LoggerFactory.getLogger(PersistenceExtensions.class)
@@ -148,7 +153,7 @@ public class PersistenceExtensions {
}
PersistenceService service = getService(effectiveServiceId);
if (service instanceof ModifiablePersistenceService modifiableService) {
modifiableService.store(item, timestamp, state);
modifiableService.store(item, timestamp, state, getAlias(item, effectiveServiceId));
return;
}
LoggerFactory.getLogger(PersistenceExtensions.class)
@@ -226,8 +231,9 @@ public class PersistenceExtensions {
internalRemoveAllStatesBetween(item, timeSeries.getBegin().atZone(timeZone),
timeSeries.getEnd().atZone(timeZone), serviceId);
}
String alias = getAlias(item, effectiveServiceId);
timeSeries.getStates()
.forEach(s -> modifiableService.store(item, s.timestamp().atZone(timeZone), s.state()));
.forEach(s -> modifiableService.store(item, s.timestamp().atZone(timeZone), s.state(), alias));
return;
}
LoggerFactory.getLogger(PersistenceExtensions.class)
@@ -316,10 +322,11 @@ public class PersistenceExtensions {
if (service instanceof QueryablePersistenceService qService) {
FilterCriteria filter = new FilterCriteria();
filter.setEndDate(timestamp);
String alias = getAlias(item, effectiveServiceId);
filter.setItemName(item.getName());
filter.setPageSize(1);
filter.setOrdering(Ordering.DESCENDING);
Iterable<HistoricItem> result = qService.query(filter);
Iterable<HistoricItem> result = qService.query(filter, alias);
if (result.iterator().hasNext()) {
return result.iterator().next();
}
@@ -453,6 +460,7 @@ public class PersistenceExtensions {
PersistenceService service = getService(effectiveServiceId);
if (service instanceof QueryablePersistenceService qService) {
FilterCriteria filter = new FilterCriteria();
String alias = getAlias(item, effectiveServiceId);
filter.setItemName(item.getName());
if (forward) {
filter.setBeginDate(ZonedDateTime.now());
@@ -465,7 +473,7 @@ public class PersistenceExtensions {
int startPage = 0;
filter.setPageNumber(startPage);
Iterable<HistoricItem> items = qService.query(filter);
Iterable<HistoricItem> items = qService.query(filter, alias);
while (items != null) {
Iterator<HistoricItem> itemIterator = items.iterator();
int itemCount = 0;
@@ -496,7 +504,7 @@ public class PersistenceExtensions {
}
if (itemCount == filter.getPageSize()) {
filter.setPageNumber(++startPage);
items = qService.query(filter);
items = qService.query(filter, alias);
} else {
items = null;
}
@@ -625,6 +633,7 @@ public class PersistenceExtensions {
PersistenceService service = getService(effectiveServiceId);
if (service instanceof QueryablePersistenceService qService) {
FilterCriteria filter = new FilterCriteria();
String alias = getAlias(item, effectiveServiceId);
filter.setItemName(item.getName());
if (forward) {
filter.setBeginDate(ZonedDateTime.now());
@@ -637,7 +646,7 @@ public class PersistenceExtensions {
int startPage = 0;
filter.setPageNumber(startPage);
Iterable<HistoricItem> items = qService.query(filter);
Iterable<HistoricItem> items = qService.query(filter, alias);
while (items != null) {
Iterator<HistoricItem> itemIterator = items.iterator();
int itemCount = 0;
@@ -650,7 +659,7 @@ public class PersistenceExtensions {
}
if (itemCount == filter.getPageSize()) {
filter.setPageNumber(++startPage);
items = qService.query(filter);
items = qService.query(filter, alias);
} else {
items = null;
}
@@ -2547,10 +2556,11 @@ public class PersistenceExtensions {
} else {
filter.setEndDate(ZonedDateTime.now());
}
String alias = getAlias(item, effectiveServiceId);
filter.setItemName(item.getName());
filter.setOrdering(Ordering.ASCENDING);
return qService.query(filter);
return qService.query(filter, alias);
} else {
LoggerFactory.getLogger(PersistenceExtensions.class)
.warn("There is no queryable persistence service registered with the id '{}'", effectiveServiceId);
@@ -2663,10 +2673,11 @@ public class PersistenceExtensions {
} else {
filter.setEndDate(ZonedDateTime.now());
}
String alias = getAlias(item, effectiveServiceId);
filter.setItemName(item.getName());
filter.setOrdering(Ordering.ASCENDING);
mService.remove(filter);
mService.remove(filter, alias);
} else {
LoggerFactory.getLogger(PersistenceExtensions.class)
.warn("There is no modifiable persistence service registered with the id '{}'", effectiveServiceId);
@@ -2743,6 +2754,15 @@ public class PersistenceExtensions {
return null;
}
private static @Nullable String getAlias(Item item, String serviceId) {
PersistenceServiceConfigurationRegistry reg = configRegistry;
if (reg != null) {
PersistenceServiceConfiguration config = reg.get(serviceId);
return config != null ? config.getAliases().get(item.getName()) : null;
}
return null;
}
private static @Nullable DecimalType getItemValue(Item item) {
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
if (baseItem instanceof NumberItem numberItem) {
@@ -188,7 +188,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
.filter(itemConfig -> itemConfig.filters().stream().allMatch(filter -> filter.apply(item)))
.forEach(itemConfig -> {
itemConfig.filters().forEach(filter -> filter.persisted(item));
container.getPersistenceService().store(item, itemConfig.alias());
container.getPersistenceService().store(item, container.getAlias(item));
}));
}
@@ -353,7 +353,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
ZonedDateTime end = timeSeries.getEnd().atZone(ZoneId.systemDefault());
FilterCriteria removeFilter = new FilterCriteria().setItemName(item.getName())
.setBeginDate(begin).setEndDate(end);
service.remove(removeFilter);
service.remove(removeFilter, container.getAlias(item));
ScheduledCompletableFuture<?> forecastJob = container.forecastJobs.get(item.getName());
if (forecastJob != null && forecastJob.getScheduledTime().isAfter(begin)
&& forecastJob.getScheduledTime().isBefore(end)) {
@@ -478,12 +478,17 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
}).stream());
}
public @Nullable String getAlias(Item item) {
return configuration.getAliases().get(item.getName());
}
private PersistenceServiceConfiguration getDefaultConfig() {
List<PersistenceStrategy> strategies = persistenceService.getDefaultStrategies();
List<PersistenceItemConfiguration> configs = List
.of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null));
return new PersistenceServiceConfiguration(persistenceService.getId(), configs, strategies, strategies,
List.of());
.of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), strategies, null));
Map<String, String> aliases = Map.of();
return new PersistenceServiceConfiguration(persistenceService.getId(), configs, aliases, strategies,
strategies, List.of());
}
/**
@@ -550,6 +555,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
private void restoreItemStateIfPossible(Item item) {
QueryablePersistenceService queryService = (QueryablePersistenceService) persistenceService;
String alias = getAlias(item);
PersistedItem persistedItem = safeCaller.create(queryService, QueryablePersistenceService.class)
.onTimeout(
@@ -558,8 +564,9 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
.onException(e -> logger.error(
"Exception occurred while querying persistence service '{}' to restore '{}': {}",
queryService.getId(), item.getName(), e.getMessage(), e))
.build().persistedItem(item.getName());
.build().persistedItem(item.getName(), alias);
if (persistedItem == null) {
// in case of an exception or timeout, the safe caller returns null
return;
}
GenericItem genericItem = (GenericItem) item;
@@ -590,6 +597,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
public void scheduleNextPersistedForecastForItem(String itemName) {
Item item = itemRegistry.get(itemName);
if (item instanceof GenericItem) {
String alias = getAlias(item);
QueryablePersistenceService queryService = (QueryablePersistenceService) persistenceService;
FilterCriteria filter = new FilterCriteria().setItemName(itemName).setBeginDate(ZonedDateTime.now())
.setOrdering(ASCENDING);
@@ -598,7 +606,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
queryService.getId(), SafeCaller.DEFAULT_TIMEOUT))
.onException(e -> logger.error("Exception occurred while querying persistence service '{}': {}",
queryService.getId(), e.getMessage(), e))
.build().query(filter).iterator();
.build().query(filter, alias).iterator();
while (result.hasNext()) {
HistoricItem next = result.next();
Instant timestamp = next.getInstant();
@@ -624,7 +632,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
if (itemConfig.filters().stream().allMatch(filter -> filter.apply(item))) {
long startTime = System.nanoTime();
itemConfig.filters().forEach(filter -> filter.persisted(item));
persistenceService.store(item, itemConfig.alias());
persistenceService.store(item, getAlias(item));
logger.trace("Storing item '{}' with persistence service '{}' took {}ms", item.getName(),
configuration.getUID(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
@@ -14,6 +14,7 @@ package org.openhab.core.persistence.registry;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.Identifiable;
@@ -25,20 +26,23 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* The {@link PersistenceServiceConfiguration} represents the configuration for a persistence service.
*
* @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
public class PersistenceServiceConfiguration implements Identifiable<String> {
private final String serviceId;
private final List<PersistenceItemConfiguration> configs;
private final Map<String, String> aliases;
private final List<PersistenceStrategy> defaults;
private final List<PersistenceStrategy> strategies;
private final List<PersistenceFilter> filters;
public PersistenceServiceConfiguration(String serviceId, Collection<PersistenceItemConfiguration> configs,
Collection<PersistenceStrategy> defaults, Collection<PersistenceStrategy> strategies,
Collection<PersistenceFilter> filters) {
Map<String, String> aliases, Collection<PersistenceStrategy> defaults,
Collection<PersistenceStrategy> strategies, Collection<PersistenceFilter> filters) {
this.serviceId = serviceId;
this.configs = List.copyOf(configs);
this.aliases = Map.copyOf(aliases);
this.defaults = List.copyOf(defaults);
this.strategies = List.copyOf(strategies);
this.filters = List.copyOf(filters);
@@ -58,6 +62,15 @@ public class PersistenceServiceConfiguration implements Identifiable<String> {
return configs;
}
/**
* Get the item aliases.
*
* @return a map of items to aliases
*/
public Map<String, String> getAliases() {
return aliases;
}
/**
* Get the default strategies.
*
@@ -46,6 +46,7 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* The {@link PersistenceServiceConfigurationDTOMapper} is a utility class to map persistence configurations for storage
*
* @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
public class PersistenceServiceConfigurationDTOMapper {
@@ -60,6 +61,7 @@ public class PersistenceServiceConfigurationDTOMapper {
dto.serviceId = persistenceServiceConfiguration.getUID();
dto.configs = persistenceServiceConfiguration.getConfigs().stream()
.map(PersistenceServiceConfigurationDTOMapper::mapPersistenceItemConfig).toList();
dto.aliases = Map.copyOf(persistenceServiceConfiguration.getAliases());
dto.defaults = persistenceServiceConfiguration.getDefaults().stream().map(PersistenceStrategy::getName)
.toList();
dto.cronStrategies = filterList(persistenceServiceConfiguration.getStrategies(), PersistenceCronStrategy.class,
@@ -100,10 +102,12 @@ public class PersistenceServiceConfigurationDTOMapper {
.map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList();
List<PersistenceFilter> filters = config.filters.stream()
.map(str -> stringToPersistenceFilter(str, filterMap, dto.serviceId)).toList();
return new PersistenceItemConfiguration(items, config.alias, strategies, filters);
return new PersistenceItemConfiguration(items, strategies, filters);
}).toList();
return new PersistenceServiceConfiguration(dto.serviceId, configs, defaults, strategyMap.values(),
Map<String, String> aliases = Map.copyOf(dto.aliases);
return new PersistenceServiceConfiguration(dto.serviceId, configs, aliases, defaults, strategyMap.values(),
filterMap.values());
}
@@ -171,7 +175,6 @@ public class PersistenceServiceConfigurationDTOMapper {
.toList();
itemDto.strategies = config.strategies().stream().map(PersistenceStrategy::getName).toList();
itemDto.filters = config.filters().stream().map(PersistenceFilter::getName).toList();
itemDto.alias = config.alias();
return itemDto;
}
@@ -16,6 +16,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.openhab.core.persistence.extensions.TestPersistenceService.*;
@@ -52,6 +53,7 @@ import org.openhab.core.library.unit.Units;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.PersistenceServiceRegistry;
import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry;
import org.openhab.core.types.State;
/**
@@ -64,6 +66,7 @@ import org.openhab.core.types.State;
* @author Mark Herwege - lastChange and nextChange methods
* @author Mark Herwege - handle persisted GroupItem with QuantityType
* @author Mark Herwege - add median methods
* @author Mark Herwege - Implement aliases
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -81,6 +84,8 @@ public class PersistenceExtensionsTest {
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock;
private @NonNullByDefault({}) GenericItem numberItem, quantityItem, groupQuantityItem, switchItem;
@BeforeEach
@@ -109,6 +114,7 @@ public class PersistenceExtensionsTest {
when(itemRegistryMock.get(TEST_SWITCH)).thenReturn(switchItem);
when(itemRegistryMock.get(TEST_GROUP_QUANTITY_NUMBER)).thenReturn(groupQuantityItem);
when(persistenceServiceConfigurationRegistryMock.get(anyString())).thenReturn(null);
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.systemDefault());
new PersistenceExtensions(new PersistenceServiceRegistry() {
@@ -135,7 +141,7 @@ public class PersistenceExtensionsTest {
public @Nullable PersistenceService get(@Nullable String serviceId) {
return TestPersistenceService.SERVICE_ID.equals(serviceId) ? testPersistenceService : null;
}
}, timeZoneProviderMock);
}, persistenceServiceConfigurationRegistryMock, timeZoneProviderMock);
}
@Test
@@ -3397,7 +3403,7 @@ public class PersistenceExtensionsTest {
public @Nullable PersistenceService get(@Nullable String serviceId) {
return TestCachedValuesPersistenceService.ID.equals(serviceId) ? persistenceService : null;
}
}, timeZoneProviderMock);
}, persistenceServiceConfigurationRegistryMock, timeZoneProviderMock);
if (historicHours > 0) {
ZonedDateTime beginHistory = now.minusHours(historicHours);
@@ -86,6 +86,7 @@ import org.openhab.core.types.UnDefType;
* The {@link PersistenceManagerTest} contains tests for the {@link PersistenceManagerImpl}
*
* @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@@ -185,8 +186,8 @@ public class PersistenceManagerTest {
when(itemRegistryMock.getItems()).thenReturn(List.of(TEST_ITEM, TEST_ITEM2, TEST_ITEM3, TEST_GROUP_ITEM));
when(persistenceServiceMock.getId()).thenReturn(TEST_PERSISTENCE_SERVICE_ID);
when(queryablePersistenceServiceMock.getId()).thenReturn(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID);
when(queryablePersistenceServiceMock.query(any())).thenReturn(List.of(TEST_HISTORIC_ITEM));
when(queryablePersistenceServiceMock.persistedItem(any())).thenReturn(TEST_PERSISTED_ITEM);
when(queryablePersistenceServiceMock.query(any(), any())).thenReturn(List.of(TEST_HISTORIC_ITEM));
when(queryablePersistenceServiceMock.persistedItem(any(), any())).thenReturn(TEST_PERSISTED_ITEM);
when(modifiablePersistenceServiceMock.getId()).thenReturn(TEST_MODIFIABLE_PERSISTENCE_SERVICE_ID);
manager = new PersistenceManagerImpl(cronSchedulerMock, schedulerMock, itemRegistryMock, safeCallerMock,
@@ -367,7 +368,7 @@ public class PersistenceManagerTest {
assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE));
assertThat(TEST_GROUP_ITEM.getLastState(), is(TEST_LAST_STATE));
verify(queryablePersistenceServiceMock, times(3)).persistedItem(any());
verify(queryablePersistenceServiceMock, times(3)).persistedItem(any(), any());
ZonedDateTime lastStateUpdate = TEST_ITEM.getLastStateUpdate();
assertNotNull(lastStateUpdate);
@@ -398,7 +399,7 @@ public class PersistenceManagerTest {
assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE));
assertThat(TEST_GROUP_ITEM.getLastState(), is(TEST_LAST_STATE));
verify(queryablePersistenceServiceMock, times(2)).persistedItem(any());
verify(queryablePersistenceServiceMock, times(2)).persistedItem(any(), any());
ZonedDateTime lastStateUpdate = TEST_ITEM.getLastStateUpdate();
assertNotNull(lastStateUpdate);
@@ -578,7 +579,7 @@ public class PersistenceManagerTest {
PersistenceStrategy strategy, @Nullable PersistenceFilter filter) {
List<PersistenceFilter> filters = filter != null ? List.of(filter) : List.of();
PersistenceItemConfiguration itemConfiguration = new PersistenceItemConfiguration(itemConfigs, null,
PersistenceItemConfiguration itemConfiguration = new PersistenceItemConfiguration(itemConfigs,
List.of(strategy), filters);
List<PersistenceStrategy> strategies = PersistenceStrategy.Globals.STRATEGIES.containsValue(strategy)
@@ -586,7 +587,7 @@ public class PersistenceManagerTest {
: List.of(strategy);
PersistenceServiceConfiguration serviceConfiguration = new PersistenceServiceConfiguration(serviceId,
List.of(itemConfiguration), List.of(), strategies, filters);
List.of(itemConfiguration), Map.of(), List.of(), strategies, filters);
manager.added(serviceConfiguration);
return serviceConfiguration;
@@ -49,6 +49,8 @@ import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.PersistenceServiceRegistry;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.registry.PersistenceServiceConfiguration;
import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry;
import org.openhab.core.types.State;
import org.openhab.core.ui.chart.ChartProvider;
import org.openhab.core.ui.internal.chart.ChartServlet;
@@ -68,6 +70,7 @@ import org.slf4j.LoggerFactory;
* @author Holger Reichert - Support for themes, DPI, legend hiding
* @author Christoph Weitkamp - Consider default persistence service
* @author Jan N. Klug - Add y-axis label formatter
* @author Mark Herwege - Implement aliases
*/
@NonNullByDefault
@Component(immediate = true)
@@ -110,12 +113,15 @@ public class DefaultChartProvider implements ChartProvider {
private final ItemUIRegistry itemUIRegistry;
private final PersistenceServiceRegistry persistenceServiceRegistry;
private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry;
@Activate
public DefaultChartProvider(final @Reference ItemUIRegistry itemUIRegistry,
final @Reference PersistenceServiceRegistry persistenceServiceRegistry) {
final @Reference PersistenceServiceRegistry persistenceServiceRegistry,
final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry) {
this.itemUIRegistry = itemUIRegistry;
this.persistenceServiceRegistry = persistenceServiceRegistry;
this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry;
if (logger.isDebugEnabled()) {
logger.debug("Available themes for default chart provider: {}", String.join(", ", CHART_THEMES.keySet()));
@@ -188,8 +194,8 @@ public class DefaultChartProvider implements ChartProvider {
// axis
styler.setAxisTickLabelsFont(chartTheme.getAxisTickLabelsFont(dpi));
styler.setAxisTickLabelsColor(chartTheme.getAxisTickLabelsColor());
styler.setXAxisMin((double) startTime.toInstant().toEpochMilli());
styler.setXAxisMax((double) endTime.toInstant().toEpochMilli());
styler.setXAxisMin(startTime.toInstant().toEpochMilli());
styler.setXAxisMax(endTime.toInstant().toEpochMilli());
int yAxisSpacing = Math.max(height / 10, chartTheme.getAxisTickLabelsFont(dpi).getSize());
if (yAxisDecimalPattern != null) {
styler.setYAxisDecimalPattern(yAxisDecimalPattern);
@@ -333,10 +339,13 @@ public class DefaultChartProvider implements ChartProvider {
// after the start of the graph (or not at all if there's no change during the graph period)
filter = new FilterCriteria();
filter.setEndDate(timeBegin);
filter.setItemName(item.getName());
String itemName = item.getName();
PersistenceServiceConfiguration config = persistenceServiceConfigurationRegistry.get(service.getId());
String alias = config != null ? config.getAliases().get(itemName) : null;
filter.setItemName(itemName);
filter.setPageSize(1);
filter.setOrdering(Ordering.DESCENDING);
result = service.query(filter);
result = service.query(filter, alias);
if (result.iterator().hasNext()) {
HistoricItem historicItem = result.iterator().next();
@@ -352,7 +361,7 @@ public class DefaultChartProvider implements ChartProvider {
filter.setOrdering(Ordering.ASCENDING);
// Get the data from the persistence store
result = service.query(filter);
result = service.query(filter, alias);
// Iterate through the data
for (HistoricItem historicItem : result) {