diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index 73b8f6a700b0e..01cad5a0aa11a 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -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. 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] []`. + +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 Discounts are automatically taken into account for channel `grid-tariff` so that it represents the actual price. diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java index 1aaaf45be5d60..0201bba12f39c 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java @@ -106,7 +106,7 @@ public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) { * @throws DataServiceException */ public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start, - Map properties) throws InterruptedException, DataServiceException { + DateQueryParameter end, Map properties) throws InterruptedException, DataServiceException { if (!SUPPORTED_CURRENCIES.contains(currency)) { throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); } @@ -119,6 +119,10 @@ public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, Da .agent(userAgent) // .method(HttpMethod.GET); + if (!end.isEmpty()) { + request = request.param("end", end.toString()); + } + try { String responseContent = sendRequest(request, properties); ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); @@ -209,9 +213,14 @@ public Collection getDatahubPriceLists(GlobalLocationNum .agent(userAgent) // .method(HttpMethod.GET); - DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter(); - if (!dateQueryParameter.isEmpty()) { - request = request.param("start", dateQueryParameter.toString()); + DateQueryParameter start = tariffFilter.getStart(); + if (!start.isEmpty()) { + request = request.param("start", start.toString()); + } + + DateQueryParameter end = tariffFilter.getEnd(); + if (!end.isEmpty()) { + request = request.param("end", end.toString()); } try { diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java index 7147e82294812..2b2ed0e318371 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java @@ -31,7 +31,7 @@ @NonNullByDefault public class EnergiDataServiceBindingConstants { - private static final String BINDING_ID = "energidataservice"; + public static final String BINDING_ID = "energidataservice"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service"); diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceComponent.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceComponent.java new file mode 100644 index 0000000000000..35a5cea9e7adf --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceComponent.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java index d2da0db906369..ef36fbdd11152 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java @@ -47,9 +47,19 @@ public PriceListParser(Clock clock) { } public Map toHourly(Collection 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 toHourly(Collection records, Instant firstHourStart, + Instant lastHourStart) { Map totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> { - Map currentMap = toHourly(records, chargeTypeCode); + Map currentMap = toHourly(records, chargeTypeCode, firstHourStart, lastHourStart); for (Entry current : currentMap.entrySet()) { BigDecimal total = totalMap.get(current.getKey()); if (total == null) { @@ -62,14 +72,10 @@ public Map toHourly(Collection reco return totalMap; } - public Map toHourly(Collection records, String chargeTypeCode) { + private Map toHourly(Collection records, String chargeTypeCode, + Instant firstHourStart, Instant lastHourStart) { Map 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 previousValidTo = LocalDateTime.MIN; Map tariffs = Map.of(); diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java index 3af139c311583..96b69d7429e6e 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java @@ -24,9 +24,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.measure.quantity.Energy; import javax.measure.quantity.Power; @@ -35,6 +33,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.energidataservice.internal.DatahubTariff; 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.handler.EnergiDataServiceHandler; import org.openhab.core.automation.annotation.ActionInput; @@ -64,44 +63,6 @@ public class EnergiDataServiceActions implements ThingActions { 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 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") public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices() { EnergiDataServiceHandler handler = this.handler; diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java index 9e53568f9e95c..09cff82b7801a 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java @@ -27,21 +27,31 @@ public class DatahubTariffFilter { private final Set chargeTypeCodes; private final Set notes; - private final DateQueryParameter dateQueryParameter; + private final DateQueryParameter start; + private final DateQueryParameter end; - public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) { - this(filter.chargeTypeCodes, filter.notes, dateQueryParameter); + public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter start) { + this(filter, start, DateQueryParameter.EMPTY); + } + + public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter start, DateQueryParameter end) { + this(filter.chargeTypeCodes, filter.notes, start, end); } public DatahubTariffFilter(Set chargeTypeCodes, Set notes) { this(chargeTypeCodes, notes, DateQueryParameter.EMPTY); } - public DatahubTariffFilter(Set chargeTypeCodes, Set notes, - DateQueryParameter dateQueryParameter) { + public DatahubTariffFilter(Set chargeTypeCodes, Set notes, DateQueryParameter start) { + this(chargeTypeCodes, notes, start, DateQueryParameter.EMPTY); + } + + public DatahubTariffFilter(Set chargeTypeCodes, Set notes, DateQueryParameter start, + DateQueryParameter end) { this.chargeTypeCodes = chargeTypeCodes; this.notes = notes; - this.dateQueryParameter = dateQueryParameter; + this.start = start; + this.end = end; } public Collection getChargeTypeCodesAsStrings() { @@ -52,7 +62,11 @@ public Collection getNotes() { return notes; } - public DateQueryParameter getDateQueryParameter() { - return dateQueryParameter; + public DateQueryParameter getStart() { + return start; + } + + public DateQueryParameter getEnd() { + return end; } } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java index 16674ae8206e7..bca4d0586d337 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java @@ -72,6 +72,14 @@ public boolean isEmpty() { return this == EMPTY; } + public @Nullable DateQueryParameterType getDateType() { + return dateType; + } + + public @Nullable LocalDate getDate() { + return date; + } + public static DateQueryParameter of(LocalDate localDate) { return new DateQueryParameter(localDate); } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java new file mode 100644 index 0000000000000..6b32f35fe318d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java @@ -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 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 getUsages() { + return Arrays.asList(buildCommandUsage(SUBCMD_UPDATE + " [" + + String.join("|", Stream.of(PriceComponent.values()).map(PriceComponent::toString).toList()) + + "] []", "Update time series in requested period")); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new EnergiDataServiceConsoleCommandCompleter(); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java index eac6f08be7bd7..f8f1af944623c 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java @@ -18,12 +18,15 @@ import java.math.BigDecimal; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.Currency; import java.util.List; import java.util.Map; @@ -41,6 +44,8 @@ import org.openhab.binding.energidataservice.internal.ApiController; import org.openhab.binding.energidataservice.internal.CacheManager; 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.api.ChargeType; import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; @@ -246,7 +251,7 @@ private void refreshElectricityPrices() { updateStatus(ThingStatus.ONLINE); updatePrices(); - updateTimeSeries(); + updateElectricityTimeSeriesFromCache(); if (isLinked(CHANNEL_SPOT_PRICE)) { long numberOfFutureSpotPrices = cacheManager.getNumberOfFutureSpotPrices(); @@ -297,7 +302,7 @@ private void downloadSpotPrices() throws InterruptedException, DataServiceExcept Map properties = editProperties(); try { ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(), - start, properties); + start, DateQueryParameter.EMPTY, properties); cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency()); } finally { updateProperties(properties); @@ -305,10 +310,7 @@ private void downloadSpotPrices() throws InterruptedException, DataServiceExcept } private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedException, DataServiceException { - GlobalLocationNumber globalLocationNumber = switch (datahubTariff) { - case GRID_TARIFF -> config.getGridCompanyGLN(); - default -> config.getEnerginetGLN(); - }; + GlobalLocationNumber globalLocationNumber = getGlobalLocationNumber(datahubTariff); if (globalLocationNumber.isEmpty()) { return; } @@ -316,17 +318,28 @@ private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedExce logger.debug("Cached tariffs of type {} still valid, skipping download.", datahubTariff); cacheManager.updateTariffs(datahubTariff); } else { - DatahubTariffFilter filter = 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(); - }; + DatahubTariffFilter filter = getDatahubTariffFilter(datahubTariff); 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 downloadPriceLists(GlobalLocationNumber globalLocationNumber, DatahubTariffFilter filter) throws InterruptedException, DataServiceException { Map properties = editProperties(); @@ -369,8 +382,8 @@ private DatahubTariffFilter getGridTariffFilter() { start); } - return new DatahubTariffFilter(filter, DateQueryParameter.of(filter.getDateQueryParameter(), - Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS))); + return new DatahubTariffFilter(filter, + DateQueryParameter.of(filter.getStart(), Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS))); } private void refreshCo2EmissionPrognosis() { @@ -494,7 +507,80 @@ private State getEnergyPrice(BigDecimal price, Currency currency) { } } - 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 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 datahubRecords = downloadPriceLists(globalLocationNumber, filter); + ZoneId zoneId = timeZoneProvider.getTimeZone(); + Instant firstHourStart = startDate.atStartOfDay(zoneId).toInstant(); + Instant lastHourStart = endDate.plusDays(1).atStartOfDay(zoneId).toInstant(); + Map 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); for (DatahubTariff datahubTariff : DatahubTariff.values()) { @@ -503,10 +589,10 @@ private void updateTimeSeries() { } } - private void updatePriceTimeSeries(String channelId, Map priceMap, Currency currency, + private int updatePriceTimeSeries(String channelId, Map priceMap, Currency currency, boolean deduplicate) { if (!isLinked(channelId)) { - return; + return 0; } List> prices = priceMap.entrySet().stream().sorted(Map.Entry.comparingByKey()) .toList(); @@ -525,6 +611,7 @@ private void updatePriceTimeSeries(String channelId, Map pr if (timeSeries.size() > 0) { sendTimeSeries(channelId, timeSeries); } + return timeSeries.size(); } /** diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java index c64cde845739a..905425a290436 100644 --- a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java @@ -97,7 +97,8 @@ void toHourlyNewTariffAtMidnight() throws IOException { PriceListParser priceListParser = new PriceListParser( Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); - Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD"); + Map tariffMap = priceListParser + .toHourly(Arrays.stream(records.records()).filter(r -> r.chargeTypeCode().equals("CD")).toList()); assertThat(tariffMap.size(), is(60)); assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717")))); @@ -110,8 +111,8 @@ void toHourlyDiscount() throws IOException { PriceListParser priceListParser = new PriceListParser( Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); - Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), - "CD R"); + Map tariffMap = priceListParser + .toHourly(Arrays.stream(records.records()).filter(r -> r.chargeTypeCode().equals("CD R")).toList()); assertThat(tariffMap.size(), is(60)); assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717"))));