From 389f6a34343a723442829b7d9841240adc2f2a9c Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Wed, 9 Oct 2024 19:32:31 +0200 Subject: [PATCH] [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 --- .../persistence/PersistenceResource.java | 41 ++++++++++++-- .../persistence/PersistenceResourceTest.java | 55 ++++++++++++++++++- .../core/persistence/dto/ItemHistoryDTO.java | 8 +++ 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java index 66a0578f2d6..c5e41e15c43 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java @@ -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.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 Response httpGetPersistenceItemData(@Context HttpHeaders headers, + 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 @@ private ZonedDateTime convertTime(String sTime) { } 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 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, } 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 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, lastState = state; } + boolean addedBoundaryEnd = false; if (boundary) { // Get the value after the end time. FilterCriteria filterAfterEnd = new FilterCriteria(); @@ -476,6 +482,31 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, 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; } } diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java index b2fd7b38ef0..767cac705d9 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java @@ -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.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.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 String getName() { @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 void testGetPersistenceItemData() { @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); diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java index e244463ea98..606c105f482 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java @@ -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 void addData(long time, State state) { 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;