Skip to content

Commit

Permalink
Introduce console command for history persistence (#16656)
Browse files Browse the repository at this point in the history
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur committed Apr 27, 2024
1 parent be7c6bb commit 321fde5
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 81 deletions.
13 changes: 13 additions & 0 deletions bundles/org.openhab.binding.energidataservice/README.md
Expand Up @@ -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] <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

Discounts are automatically taken into account for channel `grid-tariff` so that it represents the actual price.
Expand Down
Expand Up @@ -106,7 +106,7 @@ public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
* @throws DataServiceException
*/
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)) {
throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
}
Expand All @@ -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);
Expand Down Expand Up @@ -209,9 +213,14 @@ public Collection<DatahubPricelistRecord> 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 {
Expand Down
Expand Up @@ -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");
Expand Down
@@ -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;
}
}
Expand Up @@ -47,9 +47,19 @@ public PriceListParser(Clock clock) {
}

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);
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()) {
BigDecimal total = totalMap.get(current.getKey());
if (total == null) {
Expand All @@ -62,14 +72,10 @@ public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> reco
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);

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<LocalTime, BigDecimal> tariffs = Map.of();
Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<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")
public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices() {
EnergiDataServiceHandler handler = this.handler;
Expand Down
Expand Up @@ -27,21 +27,31 @@ public class DatahubTariffFilter {

private final Set<ChargeTypeCode> chargeTypeCodes;
private final Set<String> 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<ChargeTypeCode> chargeTypeCodes, Set<String> notes) {
this(chargeTypeCodes, notes, DateQueryParameter.EMPTY);
}

public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes,
DateQueryParameter dateQueryParameter) {
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes, DateQueryParameter start) {
this(chargeTypeCodes, notes, start, DateQueryParameter.EMPTY);
}

public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes, DateQueryParameter start,
DateQueryParameter end) {
this.chargeTypeCodes = chargeTypeCodes;
this.notes = notes;
this.dateQueryParameter = dateQueryParameter;
this.start = start;
this.end = end;
}

public Collection<String> getChargeTypeCodesAsStrings() {
Expand All @@ -52,7 +62,11 @@ public Collection<String> getNotes() {
return notes;
}

public DateQueryParameter getDateQueryParameter() {
return dateQueryParameter;
public DateQueryParameter getStart() {
return start;
}

public DateQueryParameter getEnd() {
return end;
}
}
Expand Up @@ -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);
}
Expand Down

0 comments on commit 321fde5

Please sign in to comment.