Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bundles/org.openhab.binding.energidataservice/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ This can be used to plan energy consumption, for example to calculate the cheape

All channels are available for thing type `service`.

## Binding Configuration

This advanced configuration option can be used if the transition to the Day-Ahead Prices dataset is postponed.
For the latest updates, please refer to the [Energi Data Service news](https://energidataservice.dk/news).

| Name | Type | Description | Default | Required |
| ---------------------- | ------- | ---------------------------------------------------------------------- | ---------- | -------- |
| dayAheadTransitionDate | text | The date when the addon switches to using the Day-Ahead Prices dataset | 2025-09-30 | no |

## Thing Configuration

### `service` Thing Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecords;
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
import org.openhab.binding.energidataservice.internal.api.dto.DayAheadPriceRecord;
import org.openhab.binding.energidataservice.internal.api.dto.DayAheadPriceRecords;
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords;
import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
Expand Down Expand Up @@ -139,6 +141,50 @@ public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, Da
}
}

/**
* Retrieve day-ahead prices for requested area and in requested {@link Currency}.
*
* @param priceArea Usually DK1 or DK2
* @param currency DKK or EUR
* @param start Specifies the start point of the period for the data request
* @param properties Map of properties which will be updated with metadata from headers
* @return Records with pairs of time start and price in requested currency.
* @throws InterruptedException
* @throws DataServiceException
*/
public DayAheadPriceRecord[] getDayAheadPrices(String priceArea, Currency currency, DateQueryParameter start,
DateQueryParameter end, Map<String, String> properties) throws InterruptedException, DataServiceException {
if (!SUPPORTED_CURRENCIES.contains(currency)) {
throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
}

Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + Dataset.DayAheadPrices)
.timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) //
.param("start", start.toString()) //
.param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") //
.param("columns", "TimeUTC,DayAheadPrice" + currency) //
.agent(userAgentSupplier.get()) //
.method(HttpMethod.GET);

if (!end.isEmpty()) {
request = request.param("end", end.toString());
}

try {
String responseContent = sendRequest(request, properties);
DayAheadPriceRecords records = gson.fromJson(responseContent, DayAheadPriceRecords.class);
if (records == null || Objects.isNull(records.records())) {
throw new DataServiceException("Error parsing response");
}

return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(DayAheadPriceRecord[]::new);
} catch (JsonSyntaxException e) {
throw new DataServiceException("Error parsing response", e);
} catch (TimeoutException | ExecutionException e) {
throw new DataServiceException(e);
}
}

private String getUserAgent() {
return "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,6 @@ public class EnergiDataServiceBindingConstants {
// Other
public static final LocalTime DAILY_REFRESH_TIME_CET = LocalTime.of(13, 0);
public static final LocalDate ENERGINET_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
public static final LocalDate DAY_AHEAD_TRANSITION_DATE = LocalDate.of(2025, 9, 30);
public static final String PROPERTY_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,25 @@
*/
@NonNullByDefault
public enum Dataset {
/**
* <a href="https://www.energidataservice.dk/tso-electricity/Elspotprices">Elspot Prices</a>
*/
SpotPrices("Elspotprices"),
/**
* <a href="https://energidataservice.dk/tso-electricity/DayAheadPrices">Day-Ahead Prices</a>
*/
DayAheadPrices("DayAheadPrices"),
/**
* <a href="https://energidataservice.dk/tso-electricity/DatahubPricelist">Datahub Price List</a>
*/
DatahubPricelist("DatahubPricelist"),
/**
* <a href="https://energidataservice.dk/tso-electricity/CO2Emis">CO2 Emission</a>
*/
CO2Emission("CO2Emis"),
/**
* <a href="https://energidataservice.dk/tso-electricity/CO2EmisProg">CO2 Emission Prognosis</a>
*/
CO2EmissionPrognosis("CO2EmisProg");

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* This class represents a dynamic date to be used in a query.
* This represents a dynamic date to be used in a query.
*
* @author Jacob Laursen - Initial contribution
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 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.api.dto;

import java.math.BigDecimal;
import java.time.Instant;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import com.google.gson.annotations.SerializedName;

/**
* Record as part of {@link DayAheadPriceRecords} from Energi Data Service.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public record DayAheadPriceRecord(@SerializedName("TimeUTC") Instant time,
@Nullable @SerializedName("DayAheadPriceDKK") BigDecimal dayAheadPriceDKK,
@Nullable @SerializedName("DayAheadPriceEUR") BigDecimal dayAheadPriceEUR) implements SpotPriceRecord {

@Override
public @Nullable BigDecimal priceDKK() {
return dayAheadPriceDKK;
}

@Override
public @Nullable BigDecimal priceEUR() {
return dayAheadPriceEUR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2010-2025 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.api.dto;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Received {@link DayAheadPriceRecords} from Energi Data Service.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public record DayAheadPriceRecords(int total, String filters, String dataset, DayAheadPriceRecord[] records) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.time.Instant;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import com.google.gson.annotations.SerializedName;

Expand All @@ -26,6 +27,21 @@
*/
@NonNullByDefault
public record ElspotpriceRecord(@SerializedName("HourUTC") Instant hour,
@SerializedName("SpotPriceDKK") BigDecimal spotPriceDKK,
@SerializedName("SpotPriceEUR") BigDecimal spotPriceEUR) {
@Nullable @SerializedName("SpotPriceDKK") BigDecimal spotPriceDKK,
@Nullable @SerializedName("SpotPriceEUR") BigDecimal spotPriceEUR) implements SpotPriceRecord {

@Override
public Instant time() {
return hour;
}

@Override
public @Nullable BigDecimal priceDKK() {
return spotPriceDKK;
}

@Override
public @Nullable BigDecimal priceEUR() {
return spotPriceEUR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2010-2025 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.api.dto;

import java.math.BigDecimal;
import java.time.Instant;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

/**
* Common interface defining a spot price.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public interface SpotPriceRecord {
/**
* Validity start, i.e. the "from" timestamp for the period, in which the values are valid.
*
* @return Validity start
*/
Instant time();

/**
* Day-Ahead price in Danish Kroner (DKK).
*
* @return Price in DKK
*/
@Nullable
BigDecimal priceDKK();

/**
* Day-Ahead price in Euro (EUR).
*
* @return price in EUR
*/
@Nullable
BigDecimal priceEUR();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;

import java.time.LocalDate;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -23,6 +25,7 @@
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
import org.openhab.binding.energidataservice.internal.provider.Co2EmissionProvider;
import org.openhab.binding.energidataservice.internal.provider.ElectricityPriceProvider;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
Expand All @@ -32,6 +35,7 @@
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;

/**
Expand All @@ -45,23 +49,33 @@
public class EnergiDataServiceHandlerFactory extends BaseThingHandlerFactory {

private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE);
private static final String DAY_AHEAD_TRANSITION_DATE_CONFIG = "dayAheadTransitionDate";

private final HttpClient httpClient;
private final TimeZoneProvider timeZoneProvider;
private final ElectricityPriceProvider electricityPriceProvider;
private final Co2EmissionProvider co2EmissionProvider;
private final DatahubTariffFilterFactory datahubTariffFilterFactory;
private final DatahubTariffFilterFactory datahubTariffFilterFactory = new DatahubTariffFilterFactory();

@Activate
public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference TimeZoneProvider timeZoneProvider,
final @Reference ElectricityPriceProvider electricityPriceProvider,
final @Reference Co2EmissionProvider co2EmissionProvider) {
final @Reference Co2EmissionProvider co2EmissionProvider, Map<String, Object> config) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.timeZoneProvider = timeZoneProvider;
this.electricityPriceProvider = electricityPriceProvider;
this.co2EmissionProvider = co2EmissionProvider;
this.datahubTariffFilterFactory = new DatahubTariffFilterFactory();

configChanged(config);
}

@Modified
public void configChanged(Map<String, Object> config) {
String dayAheadDateValue = ConfigParser.valueAs(config.get(DAY_AHEAD_TRANSITION_DATE_CONFIG), String.class);
LocalDate dayAheadDate = dayAheadDateValue != null ? LocalDate.parse(dayAheadDateValue)
: DAY_AHEAD_TRANSITION_DATE;
electricityPriceProvider.setDayAheadTransitionDate(dayAheadDate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
import org.openhab.binding.energidataservice.internal.api.dto.DayAheadPriceRecord;
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
import org.openhab.binding.energidataservice.internal.api.filter.DatahubTariffFilterFactory;
import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration;
Expand Down Expand Up @@ -442,21 +443,44 @@ public int updateSpotPriceTimeSeries(LocalDate startDate, LocalDate endDate)
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 = CURRENCY_DKK.equals(currency);
TimeSeries spotPriceTimeSeries = new TimeSeries(REPLACE);
if (spotPriceRecords.length == 0) {
return 0;
LocalDate dayAheadFirstDate = electricityPriceProvider.getDayAheadTransitionDate().plusDays(1);

if (startDate.isBefore(dayAheadFirstDate)) {
ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, currency,
DateQueryParameter.of(startDate),
DateQueryParameter
.of((endDate.isBefore(dayAheadFirstDate) ? endDate.plusDays(1) : dayAheadFirstDate)),
properties);
for (ElspotpriceRecord record : Arrays.stream(spotPriceRecords)
.sorted(Comparator.comparing(ElspotpriceRecord::hour)).toList()) {
BigDecimal spotPrice = isDKK ? record.spotPriceDKK() : record.spotPriceEUR();
if (spotPrice == null) {
continue;
}
spotPriceTimeSeries.add(record.hour(),
getEnergyPrice(spotPrice.divide(BigDecimal.valueOf(1000)), currency));
}
}
if (!endDate.isBefore(dayAheadFirstDate)) {
DayAheadPriceRecord[] spotPriceRecords = apiController.getDayAheadPrices(config.priceArea, currency,
DateQueryParameter.of(startDate.isBefore(dayAheadFirstDate) ? dayAheadFirstDate : startDate),
DateQueryParameter.of(endDate.plusDays(1)), properties);
for (DayAheadPriceRecord record : Arrays.stream(spotPriceRecords)
.sorted(Comparator.comparing(DayAheadPriceRecord::time)).toList()) {
BigDecimal spotPrice = isDKK ? record.dayAheadPriceDKK() : record.dayAheadPriceEUR();
if (spotPrice == null) {
continue;
}
spotPriceTimeSeries.add(record.time(),
getEnergyPrice(spotPrice.divide(BigDecimal.valueOf(1000)), currency));
}
}
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));
if (spotPriceTimeSeries.size() > 0) {
sendTimeSeries(CHANNEL_SPOT_PRICE, spotPriceTimeSeries);
}
sendTimeSeries(CHANNEL_SPOT_PRICE, spotPriceTimeSeries);
return spotPriceRecords.length;
return spotPriceTimeSeries.size();
} finally {
updateProperties(properties);
}
Expand Down
Loading