Introduce console command for history persistence (#16656)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-04-27 11:46:19 +02:00 committed by Ciprian Pascu
parent d10e245358
commit ba67a10c2d
11 changed files with 422 additions and 81 deletions

View File

@ -90,6 +90,19 @@ The recommended persistence strategy is `forecast`, as it ensures a clean histor
Prices from the past 24 hours and all forthcoming prices will be stored. Prices from the past 24 hours and all forthcoming prices will be stored.
Any changes that impact published prices (e.g. selecting or deselecting VAT Profile) will result in the replacement of persisted prices within this period. Any changes that impact published prices (e.g. selecting or deselecting VAT Profile) will result in the replacement of persisted prices within this period.
##### Manually Persisting History
During extended service interruptions, data unavailability, or openHAB downtime, historic prices may be absent from persistence.
A console command is provided to fill gaps: `energidataservice update [SpotPrice|GridTariff|SystemTariff|TransmissionGridTariff|ElectricityTax|ReducedElectricitytax] <StartDate> [<EndDate>]`.
Example:
```shell
energidataservice update spotprice 2024-04-12 2024-04-14
```
This can also be useful for retrospectively changing the [VAT profile](https://www.openhab.org/addons/transformations/vat/).
#### Grid Tariff #### Grid Tariff
Discounts are automatically taken into account for channel `grid-tariff` so that it represents the actual price. Discounts are automatically taken into account for channel `grid-tariff` so that it represents the actual price.

View File

@ -106,7 +106,7 @@ public class ApiController {
* @throws DataServiceException * @throws DataServiceException
*/ */
public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start, public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start,
Map<String, String> properties) throws InterruptedException, DataServiceException { DateQueryParameter end, Map<String, String> properties) throws InterruptedException, DataServiceException {
if (!SUPPORTED_CURRENCIES.contains(currency)) { if (!SUPPORTED_CURRENCIES.contains(currency)) {
throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
} }
@ -119,6 +119,10 @@ public class ApiController {
.agent(userAgent) // .agent(userAgent) //
.method(HttpMethod.GET); .method(HttpMethod.GET);
if (!end.isEmpty()) {
request = request.param("end", end.toString());
}
try { try {
String responseContent = sendRequest(request, properties); String responseContent = sendRequest(request, properties);
ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class);
@ -209,9 +213,14 @@ public class ApiController {
.agent(userAgent) // .agent(userAgent) //
.method(HttpMethod.GET); .method(HttpMethod.GET);
DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter(); DateQueryParameter start = tariffFilter.getStart();
if (!dateQueryParameter.isEmpty()) { if (!start.isEmpty()) {
request = request.param("start", dateQueryParameter.toString()); request = request.param("start", start.toString());
}
DateQueryParameter end = tariffFilter.getEnd();
if (!end.isEmpty()) {
request = request.param("end", end.toString());
} }
try { try {

View File

@ -31,7 +31,7 @@ import org.openhab.core.thing.ThingTypeUID;
@NonNullByDefault @NonNullByDefault
public class EnergiDataServiceBindingConstants { public class EnergiDataServiceBindingConstants {
private static final String BINDING_ID = "energidataservice"; public static final String BINDING_ID = "energidataservice";
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service"); public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service");

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2024 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.binding.energidataservice.internal;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link PriceComponent} represents the different components making up the total electricity price.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public enum PriceComponent {
SPOT_PRICE("SpotPrice", null),
GRID_TARIFF("GridTariff", DatahubTariff.GRID_TARIFF),
SYSTEM_TARIFF("SystemTariff", DatahubTariff.SYSTEM_TARIFF),
TRANSMISSION_GRID_TARIFF("TransmissionGridTariff", DatahubTariff.TRANSMISSION_GRID_TARIFF),
ELECTRICITY_TAX("ElectricityTax", DatahubTariff.ELECTRICITY_TAX),
REDUCED_ELECTRICITY_TAX("ReducedElectricityTax", DatahubTariff.REDUCED_ELECTRICITY_TAX);
private static final Map<String, PriceComponent> NAME_MAP = Stream.of(values())
.collect(Collectors.toMap(PriceComponent::toLowerCaseString, Function.identity()));
private String name;
private @Nullable DatahubTariff datahubTariff;
private PriceComponent(String name, @Nullable DatahubTariff datahubTariff) {
this.name = name;
this.datahubTariff = datahubTariff;
}
@Override
public String toString() {
return name;
}
private String toLowerCaseString() {
return name.toLowerCase();
}
public static PriceComponent fromString(final String name) {
PriceComponent myEnum = NAME_MAP.get(name.toLowerCase());
if (null == myEnum) {
throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
name, Arrays.asList(values())));
}
return myEnum;
}
public @Nullable DatahubTariff getDatahubTariff() {
return datahubTariff;
}
}

View File

@ -47,9 +47,19 @@ public class PriceListParser {
} }
public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records) { public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records) {
Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)
.truncatedTo(ChronoUnit.HOURS);
Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS)
.truncatedTo(ChronoUnit.DAYS);
return toHourly(records, firstHourStart, lastHourStart);
}
public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records, Instant firstHourStart,
Instant lastHourStart) {
Map<Instant, BigDecimal> totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); Map<Instant, BigDecimal> totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> { records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> {
Map<Instant, BigDecimal> currentMap = toHourly(records, chargeTypeCode); Map<Instant, BigDecimal> currentMap = toHourly(records, chargeTypeCode, firstHourStart, lastHourStart);
for (Entry<Instant, BigDecimal> current : currentMap.entrySet()) { for (Entry<Instant, BigDecimal> current : currentMap.entrySet()) {
BigDecimal total = totalMap.get(current.getKey()); BigDecimal total = totalMap.get(current.getKey());
if (total == null) { if (total == null) {
@ -62,14 +72,10 @@ public class PriceListParser {
return totalMap; return totalMap;
} }
public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records, String chargeTypeCode) { private Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records, String chargeTypeCode,
Instant firstHourStart, Instant lastHourStart) {
Map<Instant, BigDecimal> tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); Map<Instant, BigDecimal> tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)
.truncatedTo(ChronoUnit.HOURS);
Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS)
.truncatedTo(ChronoUnit.DAYS);
LocalDateTime previousValidFrom = LocalDateTime.MAX; LocalDateTime previousValidFrom = LocalDateTime.MAX;
LocalDateTime previousValidTo = LocalDateTime.MIN; LocalDateTime previousValidTo = LocalDateTime.MIN;
Map<LocalTime, BigDecimal> tariffs = Map.of(); Map<LocalTime, BigDecimal> tariffs = Map.of();

View File

@ -24,9 +24,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.quantity.Energy; import javax.measure.quantity.Energy;
import javax.measure.quantity.Power; import javax.measure.quantity.Power;
@ -35,6 +33,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.energidataservice.internal.DatahubTariff; import org.openhab.binding.energidataservice.internal.DatahubTariff;
import org.openhab.binding.energidataservice.internal.PriceCalculator; import org.openhab.binding.energidataservice.internal.PriceCalculator;
import org.openhab.binding.energidataservice.internal.PriceComponent;
import org.openhab.binding.energidataservice.internal.exception.MissingPriceException; import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
import org.openhab.core.automation.annotation.ActionInput; import org.openhab.core.automation.annotation.ActionInput;
@ -64,44 +63,6 @@ public class EnergiDataServiceActions implements ThingActions {
private @Nullable EnergiDataServiceHandler handler; private @Nullable EnergiDataServiceHandler handler;
private enum PriceComponent {
SPOT_PRICE("spotprice", null),
GRID_TARIFF("gridtariff", DatahubTariff.GRID_TARIFF),
SYSTEM_TARIFF("systemtariff", DatahubTariff.SYSTEM_TARIFF),
TRANSMISSION_GRID_TARIFF("transmissiongridtariff", DatahubTariff.TRANSMISSION_GRID_TARIFF),
ELECTRICITY_TAX("electricitytax", DatahubTariff.ELECTRICITY_TAX),
REDUCED_ELECTRICITY_TAX("reducedelectricitytax", DatahubTariff.REDUCED_ELECTRICITY_TAX);
private static final Map<String, PriceComponent> NAME_MAP = Stream.of(values())
.collect(Collectors.toMap(PriceComponent::toString, Function.identity()));
private String name;
private @Nullable DatahubTariff datahubTariff;
private PriceComponent(String name, @Nullable DatahubTariff datahubTariff) {
this.name = name;
this.datahubTariff = datahubTariff;
}
@Override
public String toString() {
return name;
}
public static PriceComponent fromString(final String name) {
PriceComponent myEnum = NAME_MAP.get(name.toLowerCase());
if (null == myEnum) {
throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
name, Arrays.asList(values())));
}
return myEnum;
}
public @Nullable DatahubTariff getDatahubTariff() {
return datahubTariff;
}
}
@RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description") @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices() { public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices() {
EnergiDataServiceHandler handler = this.handler; EnergiDataServiceHandler handler = this.handler;

View File

@ -27,21 +27,31 @@ public class DatahubTariffFilter {
private final Set<ChargeTypeCode> chargeTypeCodes; private final Set<ChargeTypeCode> chargeTypeCodes;
private final Set<String> notes; private final Set<String> notes;
private final DateQueryParameter dateQueryParameter; private final DateQueryParameter start;
private final DateQueryParameter end;
public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) { public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter start) {
this(filter.chargeTypeCodes, filter.notes, dateQueryParameter); this(filter, start, DateQueryParameter.EMPTY);
}
public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter start, DateQueryParameter end) {
this(filter.chargeTypeCodes, filter.notes, start, end);
} }
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes) { public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes) {
this(chargeTypeCodes, notes, DateQueryParameter.EMPTY); this(chargeTypeCodes, notes, DateQueryParameter.EMPTY);
} }
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes, public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes, DateQueryParameter start) {
DateQueryParameter dateQueryParameter) { this(chargeTypeCodes, notes, start, DateQueryParameter.EMPTY);
}
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes, DateQueryParameter start,
DateQueryParameter end) {
this.chargeTypeCodes = chargeTypeCodes; this.chargeTypeCodes = chargeTypeCodes;
this.notes = notes; this.notes = notes;
this.dateQueryParameter = dateQueryParameter; this.start = start;
this.end = end;
} }
public Collection<String> getChargeTypeCodesAsStrings() { public Collection<String> getChargeTypeCodesAsStrings() {
@ -52,7 +62,11 @@ public class DatahubTariffFilter {
return notes; return notes;
} }
public DateQueryParameter getDateQueryParameter() { public DateQueryParameter getStart() {
return dateQueryParameter; return start;
}
public DateQueryParameter getEnd() {
return end;
} }
} }

View File

@ -72,6 +72,14 @@ public class DateQueryParameter {
return this == EMPTY; return this == EMPTY;
} }
public @Nullable DateQueryParameterType getDateType() {
return dateType;
}
public @Nullable LocalDate getDate() {
return date;
}
public static DateQueryParameter of(LocalDate localDate) { public static DateQueryParameter of(LocalDate localDate) {
return new DateQueryParameter(localDate); return new DateQueryParameter(localDate);
} }

View File

@ -0,0 +1,173 @@
/**
* Copyright (c) 2010-2024 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.binding.energidataservice.internal.console;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.energidataservice.internal.DatahubTariff;
import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants;
import org.openhab.binding.energidataservice.internal.PriceComponent;
import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.ThingRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link EnergiDataServiceCommandExtension} is responsible for handling console commands.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class EnergiDataServiceCommandExtension extends AbstractConsoleCommandExtension {
private static final String SUBCMD_UPDATE = "update";
private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(List.of(SUBCMD_UPDATE), false);
private final ThingRegistry thingRegistry;
private class EnergiDataServiceConsoleCommandCompleter implements ConsoleCommandCompleter {
@Override
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex <= 0) {
return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
} else if (cursorArgumentIndex == 1) {
return new StringsCompleter(Stream.of(PriceComponent.values()).map(PriceComponent::toString).toList(),
false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
return false;
}
}
@Activate
public EnergiDataServiceCommandExtension(final @Reference ThingRegistry thingRegistry) {
super(EnergiDataServiceBindingConstants.BINDING_ID, "Interact with the Energi Data Service binding.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length < 1) {
printUsage(console);
return;
}
switch (args[0].toLowerCase()) {
case SUBCMD_UPDATE -> update(args, console);
default -> printUsage(console);
}
}
private void update(String[] args, Console console) {
ParsedUpdateParameters updateParameters;
try {
updateParameters = new ParsedUpdateParameters(args);
for (EnergiDataServiceHandler handler : thingRegistry.getAll().stream().map(thing -> thing.getHandler())
.filter(EnergiDataServiceHandler.class::isInstance).map(EnergiDataServiceHandler.class::cast)
.toList()) {
Instant measureStart = Instant.now();
int items = switch (updateParameters.priceComponent) {
case SPOT_PRICE ->
handler.updateSpotPriceTimeSeries(updateParameters.startDate, updateParameters.endDate);
default -> {
DatahubTariff datahubTariff = updateParameters.priceComponent.getDatahubTariff();
yield datahubTariff == null ? 0
: handler.updateTariffTimeSeries(datahubTariff, updateParameters.startDate,
updateParameters.endDate);
}
};
Instant measureEnd = Instant.now();
console.println(items + " prices updated as time series in "
+ Duration.between(measureStart, measureEnd).toMillis() + " milliseconds.");
}
} catch (InterruptedException e) {
console.println("Interrupted.");
} catch (DataServiceException e) {
console.println("Failed to fetch prices: " + e.getMessage());
} catch (IllegalArgumentException e) {
String message = e.getMessage();
if (message != null) {
console.println(message);
}
printUsage(console);
return;
}
}
private class ParsedUpdateParameters {
PriceComponent priceComponent;
LocalDate startDate;
LocalDate endDate;
private int ARGUMENT_POSITION_PRICE_COMPONENT = 1;
private int ARGUMENT_POSITION_START_DATE = 2;
private int ARGUMENT_POSITION_END_DATE = 3;
ParsedUpdateParameters(String[] args) {
if (args.length < 3 || args.length > 4) {
throw new IllegalArgumentException("Incorrect number of parameters");
}
priceComponent = PriceComponent.fromString(args[ARGUMENT_POSITION_PRICE_COMPONENT].toLowerCase());
try {
startDate = LocalDate.parse(args[ARGUMENT_POSITION_START_DATE]);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid start date: " + e.getMessage(), e);
}
try {
endDate = args.length == 3 ? startDate : LocalDate.parse(args[ARGUMENT_POSITION_END_DATE]);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid end date: " + e.getMessage(), e);
}
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("End date must be equal to or higher than start date");
}
if (endDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Future end date is not allowed");
}
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(SUBCMD_UPDATE + " ["
+ String.join("|", Stream.of(PriceComponent.values()).map(PriceComponent::toString).toList())
+ "] <StartDate> [<EndDate>]", "Update time series in requested period"));
}
@Override
public @Nullable ConsoleCommandCompleter getCompleter() {
return new EnergiDataServiceConsoleCommandCompleter();
}
}

View File

@ -18,12 +18,15 @@ import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.Currency; import java.util.Currency;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -41,6 +44,8 @@ import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.energidataservice.internal.ApiController; import org.openhab.binding.energidataservice.internal.ApiController;
import org.openhab.binding.energidataservice.internal.CacheManager; import org.openhab.binding.energidataservice.internal.CacheManager;
import org.openhab.binding.energidataservice.internal.DatahubTariff; import org.openhab.binding.energidataservice.internal.DatahubTariff;
import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants;
import org.openhab.binding.energidataservice.internal.PriceListParser;
import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions; import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions;
import org.openhab.binding.energidataservice.internal.api.ChargeType; import org.openhab.binding.energidataservice.internal.api.ChargeType;
import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
@ -246,7 +251,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
updatePrices(); updatePrices();
updateTimeSeries(); updateElectricityTimeSeriesFromCache();
if (isLinked(CHANNEL_SPOT_PRICE)) { if (isLinked(CHANNEL_SPOT_PRICE)) {
long numberOfFutureSpotPrices = cacheManager.getNumberOfFutureSpotPrices(); long numberOfFutureSpotPrices = cacheManager.getNumberOfFutureSpotPrices();
@ -297,7 +302,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
Map<String, String> properties = editProperties(); Map<String, String> properties = editProperties();
try { try {
ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(), ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(),
start, properties); start, DateQueryParameter.EMPTY, properties);
cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency()); cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency());
} finally { } finally {
updateProperties(properties); updateProperties(properties);
@ -305,10 +310,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
} }
private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedException, DataServiceException { private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedException, DataServiceException {
GlobalLocationNumber globalLocationNumber = switch (datahubTariff) { GlobalLocationNumber globalLocationNumber = getGlobalLocationNumber(datahubTariff);
case GRID_TARIFF -> config.getGridCompanyGLN();
default -> config.getEnerginetGLN();
};
if (globalLocationNumber.isEmpty()) { if (globalLocationNumber.isEmpty()) {
return; return;
} }
@ -316,17 +318,28 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
logger.debug("Cached tariffs of type {} still valid, skipping download.", datahubTariff); logger.debug("Cached tariffs of type {} still valid, skipping download.", datahubTariff);
cacheManager.updateTariffs(datahubTariff); cacheManager.updateTariffs(datahubTariff);
} else { } else {
DatahubTariffFilter filter = switch (datahubTariff) { DatahubTariffFilter filter = getDatahubTariffFilter(datahubTariff);
case GRID_TARIFF -> getGridTariffFilter();
case SYSTEM_TARIFF -> DatahubTariffFilterFactory.getSystemTariff();
case TRANSMISSION_GRID_TARIFF -> DatahubTariffFilterFactory.getTransmissionGridTariff();
case ELECTRICITY_TAX -> DatahubTariffFilterFactory.getElectricityTax();
case REDUCED_ELECTRICITY_TAX -> DatahubTariffFilterFactory.getReducedElectricityTax();
};
cacheManager.putTariffs(datahubTariff, downloadPriceLists(globalLocationNumber, filter)); cacheManager.putTariffs(datahubTariff, downloadPriceLists(globalLocationNumber, filter));
} }
} }
private DatahubTariffFilter getDatahubTariffFilter(DatahubTariff datahubTariff) {
return switch (datahubTariff) {
case GRID_TARIFF -> getGridTariffFilter();
case SYSTEM_TARIFF -> DatahubTariffFilterFactory.getSystemTariff();
case TRANSMISSION_GRID_TARIFF -> DatahubTariffFilterFactory.getTransmissionGridTariff();
case ELECTRICITY_TAX -> DatahubTariffFilterFactory.getElectricityTax();
case REDUCED_ELECTRICITY_TAX -> DatahubTariffFilterFactory.getReducedElectricityTax();
};
}
private GlobalLocationNumber getGlobalLocationNumber(DatahubTariff datahubTariff) {
return switch (datahubTariff) {
case GRID_TARIFF -> config.getGridCompanyGLN();
default -> config.getEnerginetGLN();
};
}
private Collection<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber, private Collection<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber,
DatahubTariffFilter filter) throws InterruptedException, DataServiceException { DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
Map<String, String> properties = editProperties(); Map<String, String> properties = editProperties();
@ -369,8 +382,8 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
start); start);
} }
return new DatahubTariffFilter(filter, DateQueryParameter.of(filter.getDateQueryParameter(), return new DatahubTariffFilter(filter,
Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS))); DateQueryParameter.of(filter.getStart(), Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS)));
} }
private void refreshCo2EmissionPrognosis() { private void refreshCo2EmissionPrognosis() {
@ -494,7 +507,79 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
} }
} }
private void updateTimeSeries() { /**
* Download spot prices in requested period and update corresponding channel with time series.
*
* @param startDate Start date of period
* @param endDate End date of period
* @return number of published states
*/
public int updateSpotPriceTimeSeries(LocalDate startDate, LocalDate endDate)
throws InterruptedException, DataServiceException {
if (!isLinked(CHANNEL_SPOT_PRICE)) {
return 0;
}
Map<String, String> properties = editProperties();
try {
Currency currency = config.getCurrency();
ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, currency,
DateQueryParameter.of(startDate), DateQueryParameter.of(endDate.plusDays(1)), properties);
boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency);
TimeSeries spotPriceTimeSeries = new TimeSeries(REPLACE);
if (spotPriceRecords.length == 0) {
return 0;
}
for (ElspotpriceRecord record : Arrays.stream(spotPriceRecords)
.sorted(Comparator.comparing(ElspotpriceRecord::hour)).toList()) {
spotPriceTimeSeries.add(record.hour(), getEnergyPrice(
(isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000)),
currency));
}
sendTimeSeries(CHANNEL_SPOT_PRICE, spotPriceTimeSeries);
return spotPriceRecords.length;
} finally {
updateProperties(properties);
}
}
/**
* Download tariffs in requested period and update corresponding channel with time series.
*
* @param datahubTariff Tariff to update
* @param startDate Start date of period
* @param endDate End date of period
* @return number of published states
*/
public int updateTariffTimeSeries(DatahubTariff datahubTariff, LocalDate startDate, LocalDate endDate)
throws InterruptedException, DataServiceException {
if (!isLinked(datahubTariff.getChannelId())) {
return 0;
}
GlobalLocationNumber globalLocationNumber = getGlobalLocationNumber(datahubTariff);
if (globalLocationNumber.isEmpty()) {
return 0;
}
DatahubTariffFilter filter = getDatahubTariffFilter(datahubTariff);
DateQueryParameter start = filter.getStart();
DateQueryParameterType filterStartDateType = start.getDateType();
LocalDate filterStartDate = start.getDate();
if (filterStartDateType != null) {
// For filters with date relative to current date, override with provided parameters.
filter = new DatahubTariffFilter(filter, DateQueryParameter.of(startDate), DateQueryParameter.of(endDate));
} else if (filterStartDate != null && startDate.isBefore(filterStartDate)) {
throw new IllegalArgumentException("Start date before " + start.getDate() + " is not supported");
}
Collection<DatahubPricelistRecord> datahubRecords = downloadPriceLists(globalLocationNumber, filter);
ZoneId zoneId = timeZoneProvider.getTimeZone();
Instant firstHourStart = startDate.atStartOfDay(zoneId).toInstant();
Instant lastHourStart = endDate.plusDays(1).atStartOfDay(zoneId).toInstant();
Map<Instant, BigDecimal> tariffMap = new PriceListParser().toHourly(datahubRecords, firstHourStart,
lastHourStart);
return updatePriceTimeSeries(datahubTariff.getChannelId(), tariffMap, CURRENCY_DKK, true);
}
private void updateElectricityTimeSeriesFromCache() {
updatePriceTimeSeries(CHANNEL_SPOT_PRICE, cacheManager.getSpotPrices(), config.getCurrency(), false); updatePriceTimeSeries(CHANNEL_SPOT_PRICE, cacheManager.getSpotPrices(), config.getCurrency(), false);
for (DatahubTariff datahubTariff : DatahubTariff.values()) { for (DatahubTariff datahubTariff : DatahubTariff.values()) {
@ -503,10 +588,10 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
} }
} }
private void updatePriceTimeSeries(String channelId, Map<Instant, BigDecimal> priceMap, Currency currency, private int updatePriceTimeSeries(String channelId, Map<Instant, BigDecimal> priceMap, Currency currency,
boolean deduplicate) { boolean deduplicate) {
if (!isLinked(channelId)) { if (!isLinked(channelId)) {
return; return 0;
} }
List<Entry<Instant, BigDecimal>> prices = priceMap.entrySet().stream().sorted(Map.Entry.comparingByKey()) List<Entry<Instant, BigDecimal>> prices = priceMap.entrySet().stream().sorted(Map.Entry.comparingByKey())
.toList(); .toList();
@ -525,6 +610,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
if (timeSeries.size() > 0) { if (timeSeries.size() > 0) {
sendTimeSeries(channelId, timeSeries); sendTimeSeries(channelId, timeSeries);
} }
return timeSeries.size();
} }
/** /**

View File

@ -97,7 +97,8 @@ public class PriceListParserTest {
PriceListParser priceListParser = new PriceListParser( PriceListParser priceListParser = new PriceListParser(
Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD"); Map<Instant, BigDecimal> tariffMap = priceListParser
.toHourly(Arrays.stream(records.records()).filter(r -> r.chargeTypeCode().equals("CD")).toList());
assertThat(tariffMap.size(), is(60)); assertThat(tariffMap.size(), is(60));
assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717")))); assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717"))));
@ -110,8 +111,8 @@ public class PriceListParserTest {
PriceListParser priceListParser = new PriceListParser( PriceListParser priceListParser = new PriceListParser(
Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), Map<Instant, BigDecimal> tariffMap = priceListParser
"CD R"); .toHourly(Arrays.stream(records.records()).filter(r -> r.chargeTypeCode().equals("CD R")).toList());
assertThat(tariffMap.size(), is(60)); assertThat(tariffMap.size(), is(60));
assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717")))); assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717"))));