Skip to content

Commit

Permalink
Allow sending TimeSeries for items
Browse files Browse the repository at this point in the history
Signed-off-by: Jan N. Klug <[email protected]>
  • Loading branch information
J-N-K committed May 18, 2023
1 parent d87007a commit a17f6e7
Show file tree
Hide file tree
Showing 39 changed files with 1,203 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ public String getName() {
return "restoreOnStartup";
};
};
public static final Strategy FORECAST = new StrategyImpl() {
@Override
public String getName() {
return "forecast";
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class PersistenceGlobalScopeProvider extends AbstractGlobalScopeProvider
res.getContents().add(GlobalStrategies.UPDATE);
res.getContents().add(GlobalStrategies.CHANGE);
res.getContents().add(GlobalStrategies.RESTORE);
res.getContents().add(GlobalStrategies.FORECAST);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public interface ModifiablePersistenceService extends QueryablePersistenceServic
*/
void store(Item item, ZonedDateTime date, State state);

// TODO: add a method for storing with alias to be in line with the ordinary store methods?
/**
* 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
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ public static class Globals {
public static final PersistenceStrategy UPDATE = new PersistenceStrategy("everyUpdate");
public static final PersistenceStrategy CHANGE = new PersistenceStrategy("everyChange");
public static final PersistenceStrategy RESTORE = new PersistenceStrategy("restoreOnStartup");

public static final Map<String, PersistenceStrategy> STRATEGIES = Map.of(UPDATE.name, UPDATE, CHANGE.name,
CHANGE, RESTORE.name, RESTORE);
public static final PersistenceStrategy FORECAST = new PersistenceStrategy("forecast");
public static final Map<String, PersistenceStrategy> STRATEGIES = Map.of( //
UPDATE.name, UPDATE, //
CHANGE.name, CHANGE, //
RESTORE.name, RESTORE, //
FORECAST.name, FORECAST);
}

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -32,20 +39,25 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
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.common.SafeCaller;
import org.openhab.core.common.SafeCallerBuilder;
import org.openhab.core.items.GroupItem;
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.items.StringItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.ModifiablePersistenceService;
import org.openhab.core.persistence.PersistenceItemConfiguration;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
Expand All @@ -61,10 +73,12 @@
import org.openhab.core.persistence.strategy.PersistenceStrategy;
import org.openhab.core.scheduler.CronScheduler;
import org.openhab.core.scheduler.ScheduledCompletableFuture;
import org.openhab.core.scheduler.Scheduler;
import org.openhab.core.scheduler.SchedulerRunnable;
import org.openhab.core.service.ReadyMarker;
import org.openhab.core.service.ReadyService;
import org.openhab.core.types.State;
import org.openhab.core.types.TimeSeries;
import org.openhab.core.types.UnDefType;

/**
Expand Down Expand Up @@ -108,7 +122,10 @@ public String getName() {
private static final String TEST_PERSISTENCE_SERVICE_ID = "testPersistenceService";
private static final String TEST_QUERYABLE_PERSISTENCE_SERVICE_ID = "testQueryablePersistenceService";

private static final String TEST_MODIFIABLE_PERSISTENCE_SERVICE_ID = "testModifiablePersistenceService";

private @NonNullByDefault({}) @Mock CronScheduler cronSchedulerMock;
private @NonNullByDefault({}) @Mock Scheduler schedulerMock;
private @NonNullByDefault({}) @Mock ScheduledCompletableFuture<Void> scheduledFutureMock;
private @NonNullByDefault({}) @Mock ItemRegistry itemRegistryMock;
private @NonNullByDefault({}) @Mock SafeCaller safeCallerMock;
Expand All @@ -118,6 +135,7 @@ public String getName() {

private @NonNullByDefault({}) @Mock PersistenceService persistenceServiceMock;
private @NonNullByDefault({}) @Mock QueryablePersistenceService queryablePersistenceServiceMock;
private @NonNullByDefault({}) @Mock ModifiablePersistenceService modifiablePersistenceServiceMock;

private @NonNullByDefault({}) PersistenceManager manager;

Expand All @@ -139,13 +157,15 @@ public void setUp() throws ItemNotFoundException {
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(modifiablePersistenceServiceMock.getId()).thenReturn(TEST_MODIFIABLE_PERSISTENCE_SERVICE_ID);

manager = new PersistenceManager(cronSchedulerMock, itemRegistryMock, safeCallerMock, readyServiceMock,
persistenceServiceConfigurationRegistryMock);
manager = new PersistenceManager(cronSchedulerMock, schedulerMock, itemRegistryMock, safeCallerMock,
readyServiceMock, persistenceServiceConfigurationRegistryMock);
manager.addPersistenceService(persistenceServiceMock);
manager.addPersistenceService(queryablePersistenceServiceMock);
manager.addPersistenceService(modifiablePersistenceServiceMock);

clearInvocations(persistenceServiceMock, queryablePersistenceServiceMock);
clearInvocations(persistenceServiceMock, queryablePersistenceServiceMock, modifiablePersistenceServiceMock);
}

@Test
Expand Down Expand Up @@ -299,6 +319,76 @@ public void noRestoreOnStartupWhenItemNotNull() {
verifyNoMoreInteractions(persistenceServiceMock);
}

@Test
public void storeTimeSeriesAndForecastsScheduled() {
List<ScheduledCompletableFuture<?>> futures = new ArrayList<>();

when(schedulerMock.at(any(SchedulerRunnable.class), any(Instant.class))).thenAnswer(i -> {
ScheduledCompletableFuture<?> future = mock(ScheduledCompletableFuture.class);
when(future.getScheduledTime()).thenReturn(((Instant) i.getArgument(1)).atZone(ZoneId.systemDefault()));
futures.add(future);
return future;
});

addConfiguration(TEST_MODIFIABLE_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(),
PersistenceStrategy.Globals.FORECAST, null);

Instant time1 = Instant.now().minusSeconds(1000);
Instant time2 = Instant.now().plusSeconds(1000);
Instant time3 = Instant.now().plusSeconds(2000);
Instant time4 = Instant.now().plusSeconds(3000);

// add elements

TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.ADD);
timeSeries.add(time1, new StringType("one"));
timeSeries.add(time2, new StringType("two"));
timeSeries.add(time3, new StringType("three"));
timeSeries.add(time4, new StringType("four"));

manager.timeSeriesUpdated(TEST_ITEM, timeSeries);

InOrder inOrder = inOrder(modifiablePersistenceServiceMock, schedulerMock);

inOrder.verify(modifiablePersistenceServiceMock, times(4)).store(any(Item.class), any(ZonedDateTime.class),
any(State.class));
// first element not scheduled, because it is in the past
inOrder.verify(schedulerMock).at(any(SchedulerRunnable.class), ArgumentMatchers.eq(time2));
inOrder.verify(schedulerMock).at(any(SchedulerRunnable.class), ArgumentMatchers.eq(time3));
inOrder.verify(schedulerMock).at(any(SchedulerRunnable.class), ArgumentMatchers.eq(time4));

inOrder.verifyNoMoreInteractions();

// replace elements
TimeSeries timeSeries2 = new TimeSeries(TimeSeries.Policy.REPLACE);
timeSeries2.add(time3, new StringType("three2"));
timeSeries2.add(time4, new StringType("four2"));

manager.timeSeriesUpdated(TEST_ITEM, timeSeries2);

// verify removal of old elements from service
ArgumentCaptor<FilterCriteria> filterCaptor = ArgumentCaptor.forClass(FilterCriteria.class);
inOrder.verify(modifiablePersistenceServiceMock).remove(filterCaptor.capture());
FilterCriteria filterCriteria = filterCaptor.getValue();
assertThat(filterCriteria.getItemName(), is(TEST_ITEM_NAME));
assertThat(filterCriteria.getBeginDate(), is(time3.atZone(ZoneId.systemDefault())));
assertThat(filterCriteria.getEndDate(), is(time4.atZone(ZoneId.systemDefault())));

// verify second and third restore-future are cancelled
verify(futures.get(0), never()).cancel(anyBoolean());
verify(futures.get(1)).cancel(true);
verify(futures.get(2)).cancel(true);

// verify new values are stored
inOrder.verify(modifiablePersistenceServiceMock, times(2)).store(any(Item.class), any(ZonedDateTime.class),
any(State.class));
// verify new restore futures are scheduled
inOrder.verify(schedulerMock).at(any(SchedulerRunnable.class), ArgumentMatchers.eq(time3));
inOrder.verify(schedulerMock).at(any(SchedulerRunnable.class), ArgumentMatchers.eq(time4));

inOrder.verifyNoMoreInteractions();
}

@Test
public void cronStrategyIsScheduledAndCancelledAndPersistsValue() throws Exception {
ArgumentCaptor<SchedulerRunnable> runnableCaptor = ArgumentCaptor.forClass(SchedulerRunnable.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class MagicBindingConstants {
public static final ThingTypeUID THING_TYPE_DYNAMIC_STATE_DESCRIPTION = new ThingTypeUID(BINDING_ID,
"dynamic-state-description");
public static final ThingTypeUID THING_TYPE_ONLINE_OFFLINE = new ThingTypeUID(BINDING_ID, "online-offline");
public static final ThingTypeUID THING_TYPE_TIMESERIES = new ThingTypeUID(BINDING_ID, "timeseries");

// bridged things
public static final ThingTypeUID THING_TYPE_BRIDGE_1 = new ThingTypeUID(BINDING_ID, "magic-bridge1");
Expand All @@ -67,7 +68,7 @@ public class MagicBindingConstants {
public static final String CHANNEL_BATTERY_LEVEL = "battery-level";
public static final String CHANNEL_SYSTEM_COMMAND = "systemcommand";
public static final String CHANNEL_SIGNAL_STRENGTH = "signal-strength";

public static final String CHANNEL_FORECAST = "forecast";
// Firmware update needed models
public static final String UPDATE_MODEL_PROPERTY = "updateModel";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* 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.core.magic.binding.handler;

import static org.openhab.core.magic.binding.MagicBindingConstants.CHANNEL_FORECAST;
import static org.openhab.core.types.TimeSeries.Policy.ADD;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.TimeSeries;

/**
* The {@link MagicTimeSeriesHandler} is capable of providing a series of different forecasts
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class MagicTimeSeriesHandler extends BaseThingHandler {

private @Nullable ScheduledFuture<?> scheduledJob;
private Configuration configuration = new Configuration();

public MagicTimeSeriesHandler(Thing thing) {
super(thing);
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no-op
}

@Override
public void initialize() {
configuration = getConfigAs(Configuration.class);
startScheduledJob();

updateStatus(ThingStatus.ONLINE);
}

@Override
public void dispose() {
stopScheduledJob();
}

private void startScheduledJob() {
ScheduledFuture<?> localScheduledJob = scheduledJob;
if (localScheduledJob == null || localScheduledJob.isCancelled()) {
scheduledJob = scheduler.scheduleWithFixedDelay(() -> {
Instant now = Instant.now();
TimeSeries timeSeries = new TimeSeries(ADD);
Duration stepSize = Duration.ofSeconds(configuration.interval / configuration.count);
double range = configuration.max - configuration.min;
for (int i = 1; i <= configuration.count; i++) {
double value = switch (configuration.type) {
case RND -> Math.random() * range + configuration.min;
case ASC -> (range / configuration.count) * i + configuration.min;
case DESC -> configuration.max + (range / configuration.count) * i;
};
timeSeries.add(now.plus(stepSize.multipliedBy(i)), new DecimalType(value));
}
sendTimeSeries(CHANNEL_FORECAST, timeSeries);
}, 0, configuration.interval, TimeUnit.SECONDS);
}
}

private void stopScheduledJob() {
ScheduledFuture<?> localScheduledJob = scheduledJob;
if (localScheduledJob != null && !localScheduledJob.isCancelled()) {
localScheduledJob.cancel(true);
scheduledJob = null;
}
}

public static class Configuration {
public int interval = 600;
public Type type = Type.RND;
public double min = 0.0;
public double max = 100.0;
public int count = 10;

public Configuration() {
}
}

public enum Type {
RND,
ASC,
DESC
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.openhab.core.magic.binding.handler.MagicPlayerHandler;
import org.openhab.core.magic.binding.handler.MagicRollershutterHandler;
import org.openhab.core.magic.binding.handler.MagicThermostatThingHandler;
import org.openhab.core.magic.binding.handler.MagicTimeSeriesHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
Expand All @@ -62,8 +63,8 @@ public class MagicHandlerFactory extends BaseThingHandlerFactory {
THING_TYPE_CONTACT_SENSOR, THING_TYPE_CONFIG_THING, THING_TYPE_DELAYED_THING, THING_TYPE_LOCATION,
THING_TYPE_THERMOSTAT, THING_TYPE_FIRMWARE_UPDATE, THING_TYPE_BRIDGE_1, THING_TYPE_BRIDGE_2,
THING_TYPE_BRIDGED_THING, THING_TYPE_CHATTY_THING, THING_TYPE_ROLLERSHUTTER, THING_TYPE_PLAYER,
THING_TYPE_IMAGE, THING_TYPE_ACTION_MODULE, THING_TYPE_DYNAMIC_STATE_DESCRIPTION,
THING_TYPE_ONLINE_OFFLINE);
THING_TYPE_IMAGE, THING_TYPE_ACTION_MODULE, THING_TYPE_DYNAMIC_STATE_DESCRIPTION, THING_TYPE_ONLINE_OFFLINE,
THING_TYPE_TIMESERIES);

private final MagicDynamicCommandDescriptionProvider commandDescriptionProvider;
private final MagicDynamicStateDescriptionProvider stateDescriptionProvider;
Expand Down Expand Up @@ -125,6 +126,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return new MagicOnlineOfflineHandler(thing);
} else if (THING_TYPE_BRIDGE_1.equals(thingTypeUID) || THING_TYPE_BRIDGE_2.equals(thingTypeUID)) {
return new MagicBridgeHandler((Bridge) thing);
} else if (THING_TYPE_TIMESERIES.equals(thingTypeUID)) {
return new MagicTimeSeriesHandler(thing);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,8 @@
<category>time</category>
</channel-type>

<channel-type id="forecast">
<item-type>Number</item-type>
<label>Forecast</label>
</channel-type>
</thing:thing-descriptions>
Loading

0 comments on commit a17f6e7

Please sign in to comment.