Skip to content

Commit

Permalink
[rest] Persistence: Optionally add current Item state to response (#4394
Browse files Browse the repository at this point in the history
)

* [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 <[email protected]>
  • Loading branch information
florian-h05 authored Oct 9, 2024
1 parent 963a8d1 commit 389f6a3
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 389f6a3

Please sign in to comment.