[rest] Persistence: Optionally add current Item state to response (#4394)

* [rest] Persistence endpoint: Optionally add current Item state to response

This new optional parameter gives the UI additional possibilities for charts.
E.g. it is now possible to display a bar chart with monthly energy consumption, where the consumption is only persisted at the end of the month, that includes the data from this month.

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
Florian Hotze
2024-10-09 19:32:31 +02:00
committed by GitHub
parent 963a8d1a5f
commit 389f6a3434
3 changed files with 97 additions and 7 deletions
@@ -12,6 +12,7 @@
*/
package org.openhab.core.io.rest.core.internal.persistence;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
@@ -71,6 +72,7 @@ import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegi
import org.openhab.core.persistence.strategy.PersistenceStrategy;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
import org.openhab.core.types.UnDefType;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -294,8 +296,9 @@ public class PersistenceResource implements RESTResource {
+ DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]") @QueryParam("endtime") @Nullable String endTime,
@Parameter(description = "Page number of data to return. This parameter will enable paging.") @QueryParam("page") int pageNumber,
@Parameter(description = "The length of each page.") @QueryParam("pagelength") int pageLength,
@Parameter(description = "Gets one value before and after the requested period.") @QueryParam("boundary") boolean boundary) {
return getItemHistoryDTO(serviceId, itemName, startTime, endTime, pageNumber, pageLength, boundary);
@Parameter(description = "Gets one value before and after the requested period.") @QueryParam("boundary") boolean boundary,
@Parameter(description = "Adds the current Item state into the requested period (the item state will be before or at the endtime)") @QueryParam("itemState") boolean itemState) {
return getItemHistoryDTO(serviceId, itemName, startTime, endTime, pageNumber, pageLength, boundary, itemState);
}
@DELETE
@@ -341,12 +344,13 @@ public class PersistenceResource implements RESTResource {
}
private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, @Nullable String timeBegin,
@Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary) {
@Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary, boolean itemState) {
// Benchmarking timer...
long timerStart = System.currentTimeMillis();
@Nullable
ItemHistoryDTO dto = createDTO(serviceId, itemName, timeBegin, timeEnd, pageNumber, pageLength, boundary);
ItemHistoryDTO dto = createDTO(serviceId, itemName, timeBegin, timeEnd, pageNumber, pageLength, boundary,
itemState);
if (dto == null) {
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
@@ -359,7 +363,8 @@ public class PersistenceResource implements RESTResource {
}
protected @Nullable ItemHistoryDTO createDTO(@Nullable String serviceId, String itemName,
@Nullable String timeBegin, @Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary) {
@Nullable String timeBegin, @Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary,
boolean itemState) {
// If serviceId is null, then use the default service
PersistenceService service;
String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId();
@@ -465,6 +470,7 @@ public class PersistenceResource implements RESTResource {
lastState = state;
}
boolean addedBoundaryEnd = false;
if (boundary) {
// Get the value after the end time.
FilterCriteria filterAfterEnd = new FilterCriteria();
@@ -476,6 +482,31 @@ public class PersistenceResource implements RESTResource {
if (result.iterator().hasNext()) {
dto.addData(dateTimeEnd.toInstant().toEpochMilli(), result.iterator().next().getState());
quantity++;
addedBoundaryEnd = true;
}
}
// only add the item state if it was requested and the boundary end was not added
// if the boundary end was added, there is no need to add the item state moved to the end time
if (itemState && !addedBoundaryEnd) {
try {
long time = Instant.now().toEpochMilli();
// if the current time is after the requested end time, move the item state to the end time
if (time > dateTimeEnd.toInstant().toEpochMilli()) {
time = dateTimeEnd.toInstant().toEpochMilli();
}
State state = itemRegistry.getItem(itemName).getState();
if (state instanceof UnDefType) {
logger.debug("State of item '{}' is undefined, not adding it to the response.", itemName);
} else {
logger.debug("Adding state of item '{}' to the response: {} - {}", itemName, time, state);
dto.addData(time, state);
quantity++;
dto.sortData();
}
} catch (ItemNotFoundException e) {
logger.debug("Item '{}' not found, not adding the state to the response.", itemName);
return null;
}
}
@@ -13,6 +13,7 @@
package org.openhab.core.io.rest.core.internal.persistence;
import static org.hamcrest.CoreMatchers.is;
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.*;
@@ -40,6 +41,7 @@ import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.ModifiablePersistenceService;
@@ -50,6 +52,7 @@ import org.openhab.core.persistence.internal.PersistenceManagerImpl;
import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider;
import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Tests for PersistenceItem Restresource
@@ -74,6 +77,7 @@ public class PersistenceResourceTest {
private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock;
private @Mock @NonNullByDefault({}) ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProviderMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) Item itemMock;
@BeforeEach
public void beforeEach() {
@@ -116,7 +120,7 @@ public class PersistenceResourceTest {
@Test
public void testGetPersistenceItemData() {
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, false);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, false, false);
assertThat(Integer.parseInt(dto.datapoints), is(5));
assertThat(dto.data, hasSize(5));
@@ -147,12 +151,59 @@ public class PersistenceResourceTest {
@Test
public void testGetPersistenceItemDataWithBoundery() {
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, true);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, true, false);
assertThat(Integer.parseInt(dto.datapoints), is(7));
assertThat(dto.data, hasSize(7));
}
@Test
public void testGetPersistenceItemDataWithItemState() throws ItemNotFoundException {
when(itemRegistryMock.getItem("testItem")).thenReturn(itemMock);
when(itemMock.getState()).thenReturn(DecimalType.ZERO);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, false, true);
assertThat(Integer.parseInt(dto.datapoints), is(6));
assertThat(dto.data, hasSize(6));
assertThat(dto.data.get(dto.data.size() - 1).state, is("0"));
}
@Test
public void testGetPersistenceItemDataWithItemStateUndefined() throws ItemNotFoundException {
when(itemRegistryMock.getItem("testItem")).thenReturn(itemMock);
when(itemMock.getState()).thenReturn(UnDefType.UNDEF);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, false, true);
assertThat(Integer.parseInt(dto.datapoints), is(5));
assertThat(dto.data, hasSize(5));
}
@Test
public void testGetPersistenceItemDataWithItemStateNull() throws ItemNotFoundException {
when(itemRegistryMock.getItem("testItem")).thenReturn(itemMock);
when(itemMock.getState()).thenReturn(UnDefType.NULL);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, false, true);
assertThat(Integer.parseInt(dto.datapoints), is(5));
assertThat(dto.data, hasSize(5));
}
@Test
public void testGetPersistenceItemDataWithBoundaryAndItemStateButNoItemStateRequired()
throws ItemNotFoundException {
when(itemRegistryMock.getItem("testItem")).thenReturn(itemMock);
when(itemMock.getState()).thenReturn(DecimalType.ZERO);
ItemHistoryDTO dto = pResource.createDTO(PERSISTENCE_SERVICE_ID, "testItem", null, null, 1, 10, true, true);
assertThat(Integer.parseInt(dto.datapoints), is(7));
assertThat(dto.data, hasSize(7));
assertThat(dto.data.get(dto.data.size() - 1).state, not("0"));
}
@Test
public void testPutPersistenceItemData() throws ItemNotFoundException {
HttpHeaders headersMock = mock(HttpHeaders.class);
@@ -13,6 +13,7 @@
package org.openhab.core.persistence.dto;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.openhab.core.library.types.DecimalType;
@@ -57,6 +58,13 @@ public class ItemHistoryDTO {
data.add(newVal);
}
/**
* Sort the data history by time.
*/
public void sortData() {
data.sort(Comparator.comparingLong(o -> o.time));
}
public static class HistoryDataBean {
public long time;
public String state;