diff --git a/CODEOWNERS b/CODEOWNERS
index e1fe2044e900b..ca4787aa8b82e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -88,6 +88,7 @@
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.elroconnects/ @mherwege
/bundles/org.openhab.binding.energenie/ @hmerk
+/bundles/org.openhab.binding.energidataservice/ @jlaur
/bundles/org.openhab.binding.enigma2/ @gdolfen
/bundles/org.openhab.binding.enocean/ @fruggy83
/bundles/org.openhab.binding.enphase/ @Hilbrand
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index d446bf5b49ed9..bcc9af79f2ded 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -431,6 +431,11 @@
org.openhab.binding.energenie
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.energidataservice
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.enigma2
diff --git a/bundles/org.openhab.binding.energidataservice/NOTICE b/bundles/org.openhab.binding.energidataservice/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md
new file mode 100644
index 0000000000000..b49b2c8e58b15
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/README.md
@@ -0,0 +1,408 @@
+# Energi Data Service Binding
+
+This binding integrates electricity prices from the Danish Energi Data Service ("Open energy data from Energinet to society").
+
+This can be used to plan energy consumption, for example to calculate the cheapest period for running a dishwasher or charging an EV.
+
+## Supported Things
+
+All channels are available for thing type `service`.
+
+## Thing Configuration
+
+### `service` Thing Configuration
+
+| Name | Type | Description | Default | Required |
+|----------------|---------|---------------------------------------------------|---------------|----------|
+| priceArea | text | Price area for spot prices (same as bidding zone) | | yes |
+| currencyCode | text | Currency code in which to obtain spot prices | DKK | no |
+| gridCompanyGLN | integer | Global Location Number of the Grid Company | | no |
+| energinetGLN | integer | Global Location Number of Energinet | 5790000432752 | no |
+
+#### Global Location Number of the Grid Company
+
+The Global Location Number of your grid company can be selected from a built-in list of grid companies.
+To find the company in your area, you can go to [Find netselskab](https://greenpowerdenmark.dk/vejledning-teknik/nettilslutning/find-netselskab), enter your address, and the company will be shown.
+
+If your company is not on the list, you can configure it manually.
+To obtain the Global Location Number of your grid company:
+
+- Open a browser and go to [Eloverblik](https://eloverblik.dk/).
+- Click "Private customers" and log in with MitID (confirmation will appear as Energinet).
+- Click "Retrieve data" and select "Price data".
+- Open the file and look for the rows having **Price_type** = "Subscription".
+- In the columns **Name** and/or **Description** you should see the name of your grid company.
+- In column **Owner** you can find the GLN ("Global Location Number").
+- Most rows will have this **Owner**. If in doubt, try to look for rows __not__ having 5790000432752 as owner.
+
+## Channels
+
+### Channel Group `electricity`
+
+| Channel | Type | Description | Advanced |
+|------------------------------|--------|------------------------------------------------------------------------------------------------|----------|
+| currentSpotPrice | Number | Spot price in DKK or EUR per kWh for current hour | no |
+| currentNetTariff | Number | Net tariff in DKK per kWh for current hour. Only available when `gridCompanyGLN` is configured | no |
+| currentSystemTariff | Number | System tariff in DKK per kWh for current hour | no |
+| currentElectricityTax | Number | Electricity tax in DKK per kWh for current hour | no |
+| currentTransmissionNetTariff | Number | Transmission net tariff in DKK per kWh for current hour | no |
+| hourlyPrices | String | JSON array with hourly prices from 12 hours ago and onward | yes |
+
+_Please note:_ There is no channel providing the total price.
+Instead, create a group item with `SUM` as aggregate function and add the individual price items as children.
+This has the following advantages:
+
+- Full customization possible: Freely choose the channels which should be included in the total.
+- An additional item containing the kWh fee from your electricity supplier can be added also.
+- Spot price can be configured in EUR while tariffs are in DKK.
+
+#### Value-Added Tax
+
+The channels `currentSpotPrice`, `currentNetTariff`, `currentSystemTariff`, `currentElectricityTax` and `currentTransmissionNetTariff` can be configured to include VAT with this configuration parameter:
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|----------------------------------------------|---------|----------|----------|
+| includeVAT | boolean | Add VAT to amount based on regional settings | no | no | no |
+
+Please be aware that this channel configuration will affect all linked items.
+
+#### Current Net Tariff
+
+Discounts are automatically taken into account for channel `currentNetTariff` so that it represents the actual price.
+
+The tariffs are downloaded using pre-configured filters for the different [Grid Company GLN's](#global-location-number-of-the-grid-company).
+If your company is not in the list, or the filters are not working, they can be manually overridden.
+To override filters, the channel `currentNetTariff` has the following configuration parameters:
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|----------------------------------------------------------------------------------------------------------------------------|---------|----------|----------|
+| chargeTypeCodes | text | Comma-separated list of charge type codes | | no | yes |
+| notes | text | Comma-separated list of notes | | no | yes |
+| start | text | Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear | | no | yes |
+
+The parameters `chargeTypeCodes` and `notes` are logically combined with "AND", so if only one parameter is needed for filter, only provide this parameter and leave the other one empty.
+Using any of these parameters will override the pre-configured filter entirely.
+
+The parameter `start` can be used independently to override the query start date parameter.
+If used while leaving `chargeTypeCodes` and `notes` empty, only the date will be overridden.
+
+Determining the right filters can be tricky, so if in doubt ask in the community forum.
+See also [Datahub Price List](https://www.energidataservice.dk/tso-electricity/DatahubPricelist).
+
+##### Filter Examples
+
+_N1:_
+| Parameter | Value |
+|-----------------|------------|
+| chargeTypeCodes | CD,CD R |
+| notes | |
+
+_Nord Energi Net:_
+| Parameter | Value |
+|-----------------|------------|
+| chargeTypeCodes | TA031U200 |
+| notes | Nettarif C |
+
+#### Hourly Prices
+
+The format of the `hourlyPrices` JSON array is as follows:
+
+```json
+[
+ {
+ "hourStart": "2023-01-24T15:00:00Z",
+ "spotPrice": 1.67076001,
+ "spotPriceCurrency": "DKK",
+ "netTariff": 0.432225,
+ "systemTariff": 0.054000,
+ "electricityTax": 0.008000,
+ "transmissionNetTariff": 0.058000
+ },
+ {
+ "hourStart": "2023-01-24T16:00:00Z",
+ "spotPrice": 1.859880005,
+ "spotPriceCurrency": "DKK",
+ "netTariff": 1.05619,
+ "systemTariff": 0.054000,
+ "electricityTax": 0.008000,
+ "transmissionNetTariff": 0.058000
+ }
+]
+```
+
+Future spot prices for the next day are usually available around 13:00 CET and are fetched around that time.
+Historic prices older than 12 hours are removed from the JSON array each hour.
+Channel configuration for "Include VAT" is ignored, i.e. VAT is excluded.
+
+## Thing Actions
+
+Thing actions can be used to perform calculations as well as import prices directly into rules without deserializing JSON from the [hourlyPrices](#hourly-prices) channel.
+This is more convenient, much faster, and provides automatic summation of the price elements of interest.
+
+Actions use cached data for performing operations.
+Since data is only fetched when an item is linked to a channel, there might not be any cached data available.
+In this case the data will be fetched on demand and cached afterwards.
+The first action triggered on a given day may therefore be a bit slower, and is also prone to failing if the server call fails for any reason.
+This potential problem can be prevented by linking the indivial channels to items, or by linking the `hourlyPrices` channel to an item.
+
+### `calculateCheapestPeriod`
+
+This action will determine the cheapest period for using energy.
+It comes in four variants with different input parameters.
+
+The result is a `Map` with the following keys:
+
+| Key | Type | Description |
+|--------------------|--------------|-------------------------------------------------------|
+| CheapestStart | `Instant` | Start time of cheapest calculated period |
+| LowestPrice | `BigDecimal` | The total price when starting at cheapest start |
+| MostExpensiveStart | `Instant` | Start time of most expensive calculated period |
+| HighestPrice | `BigDecimal` | The total price when starting at most expensive start |
+
+#### `calculateCheapestPeriod` from Duration
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| earliestStart | `Instant` | Earliest start time allowed |
+| latestEnd | `Instant` | Latest end time allowed |
+| duration | `Duration` | The duration to fit within the timeslot |
+
+This is a convenience method that can be used when the power consumption is not known.
+The calculation will assume linear consumption and will find the best timeslot based on that.
+The this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`.
+
+Example:
+
+```javascript
+var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90))
+```
+
+#### `calculateCheapestPeriod` from Duration and Power
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| earliestStart | `Instant` | Earliest start time allowed |
+| latestEnd | `Instant` | Latest end time allowed |
+| duration | `Duration` | The duration to fit within the timeslot |
+| power | `QuantityType` | Linear power consumption |
+
+This action is identical the the variant above, but with a known linear power consumption.
+As a result the price is also included in the result.
+
+Example:
+
+```javascript
+var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90), 250 | W)
+```
+
+#### `calculateCheapestPeriod` from Power Phases
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| earliestStart | `Instant` | Earliest start time allowed |
+| latestEnd | `Instant` | Latest end time allowed |
+| durationPhases | `List` | List of durations for the phases |
+| powerPhases | `List>` | List of power consumption for each corresponding phase |
+
+This variant is similar to the one above, but is based on a supplied timetable.
+
+The timetable is supplied as two individual parameters, `durationPhases` and `powerPhases`, which must have the same size.
+This can be considered as different phases of using power, so each list member represents a period with a linear use of power.
+`durationPhases` should be a List populated by `Duration` objects, while `powerPhases` should be a List populated by `QuantityType` objects for that duration of time.
+
+Example:
+
+```javascript
+val ArrayList durationPhases = new ArrayList()
+durationPhases.add(Duration.ofMinutes(37))
+durationPhases.add(Duration.ofMinutes(8))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(2))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(36))
+durationPhases.add(Duration.ofMinutes(41))
+durationPhases.add(Duration.ofMinutes(104))
+
+val ArrayList> powerPhases = new ArrayList>()
+powerPhases.add(162.162 | W)
+powerPhases.add(750 | W)
+powerPhases.add(1500 | W)
+powerPhases.add(3000 | W)
+powerPhases.add(1500 | W)
+powerPhases.add(166.666 | W)
+powerPhases.add(146.341 | W)
+powerPhases.add(0 | W)
+
+var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), durationPhases, powerPhases)
+```
+
+Please note that the total duration will be calculated automatically as a sum of provided duration phases.
+Therefore, if the total duration is longer than the sum of phase durations, the remaining duration must be provided as last item with a corresponding 0 W power item.
+This is to ensure that the full program will finish before the provided `latestEnd`.
+
+#### `calculateCheapestPeriod` from Energy per Phase
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| earliestStart | `Instant` | Earliest start time allowed |
+| latestEnd | `Instant` | Latest end time allowed |
+| totalDuration | `Duration` | The total duration of all phases |
+| durationPhases | `List` | List of durations for the phases |
+| energyUsedPerPhase | `QuantityType` | Fixed amount of energy used per phase |
+
+This variant will assign the provided amount of energy into each phase.
+The use case for this variant is a simplification of the previous variant.
+For example, a dishwasher may provide energy consumption in 0.1 kWh steps.
+In this case it's a simple task to create a timetable accordingly without having to calculate the average power consumption per phase.
+Since a last phase may use no significant energy, the total duration must be provided also.
+
+Example:
+
+```javascript
+val ArrayList durationPhases = new ArrayList()
+durationPhases.add(Duration.ofMinutes(37))
+durationPhases.add(Duration.ofMinutes(8))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(2))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(36))
+durationPhases.add(Duration.ofMinutes(41))
+
+// 0.7 kWh is used in total (number of phases × energy used per phase)
+var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh)
+```
+
+### `calculatePrice`
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| start | `Instant` | Start time |
+| end | `Instant` | End time |
+| power | `QuantityType` | Linear power consumption |
+
+**Result:** Price as `BigDecimal`.
+
+This action calculates the price for using given amount of power in the period from `start` till `end`.
+
+Example:
+
+```javascript
+var price = actions.calculatePrice(now.toInstant(), now.plusHours(4).toInstant, 200 | W)
+```
+
+### `getPrices`
+
+| Parameter | Type | Description |
+|--------------------|-----------------------------|--------------------------------------------------------|
+| priceElements | `String` | Comma-separated list of price elements to include |
+
+**Result:** `Map`
+
+The parameter `priceElements` is a case-insensitive comma-separated list of price elements to include in the returned hourly prices.
+These elements can be requested:
+
+| Price element | Description |
+|-----------------------|-------------------------|
+| SpotPrice | Spot price |
+| NetTariff | Net tariff |
+| SystemTariff | System tariff |
+| ElectricityTax | Electricity tax |
+| TransmissionNetTariff | Transmission net tariff |
+
+Using `null` as parameter returns the total prices including all price elements.
+
+Example:
+
+```javascript
+var priceMap = actions.getPrices("SpotPrice,NetTariff");
+```
+
+## Full Example
+
+### Thing Configuration
+
+```java
+Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] {
+ Channels:
+ Number : electricity#currentSpotPrice [ includeVAT="true" ]
+ Number : electricity#currentNetTariff [ includeVAT="true" ]
+ Number : electricity#currentSystemTariff [ includeVAT="true" ]
+ Number : electricity#currentElectricityTax [ includeVAT="true" ]
+ Number : electricity#currentTransmissionNetTariff [ includeVAT="true" ]
+}
+```
+
+### Item Configuration
+
+```java
+Group:Number:SUM CurrentTotalPrice "Current Total Price"
+Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSpotPrice"}
+Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentNetTariff"}
+Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSystemTariff"}
+Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentElectricityTax"}
+Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentTransmissionNetTariff"}
+String HourlyPrices "Hourly Prices" {channel="energidataservice:service:energidataservice:electricity#hourlyPrices"}
+```
+
+### Thing Actions Example
+
+```javascript
+import java.time.Duration
+import java.util.ArrayList
+import java.util.Map
+import java.time.temporal.ChronoUnit
+
+val actions = getActions("energidataservice", "energidataservice:service:energidataservice");
+
+var priceMap = actions.getPrices(null);
+var hourStart = now.toInstant().truncatedTo(ChronoUnit.HOURS)
+logInfo("Current total price excl. VAT", priceMap.get(hourStart).toString)
+
+var priceMap = actions.getPrices("SpotPrice,NetTariff");
+logInfo("Current spot price + net tariff excl. VAT", priceMap.get(hourStart).toString)
+
+var price = actions.calculatePrice(Instant.now, now.plusHours(1).toInstant, 150 | W)
+logInfo("Total price for using 150 W for the next hour", price.toString)
+
+val ArrayList durationPhases = new ArrayList()
+durationPhases.add(Duration.ofMinutes(37))
+durationPhases.add(Duration.ofMinutes(8))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(2))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(36))
+durationPhases.add(Duration.ofMinutes(41))
+durationPhases.add(Duration.ofMinutes(104))
+
+val ArrayList> consumptionPhases = new ArrayList>()
+consumptionPhases.add(162.162 | W)
+consumptionPhases.add(750 | W)
+consumptionPhases.add(1500 | W)
+consumptionPhases.add(3000 | W)
+consumptionPhases.add(1500 | W)
+consumptionPhases.add(166.666 | W)
+consumptionPhases.add(146.341 | W)
+consumptionPhases.add(0 | W)
+
+var Map result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases)
+logInfo("Lowest price", result.get("LowestPrice"))
+logInfo("Cheapest start", result.get("CheapestStart"))
+logInfo("Highest price price", result.get("HighestPrice"))
+logInfo("Most expensive start", result.get("MostExpensiveStart"))
+
+// This is a simpler version taking advantage of the fact that each interval here represents 0.1 kWh of consumed energy.
+// In this example we have to provide the total duration to make sure we fit the latest end. This is because there is no
+// registered consumption in the last phase.
+val ArrayList durationPhases = new ArrayList()
+durationPhases.add(Duration.ofMinutes(37))
+durationPhases.add(Duration.ofMinutes(8))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(2))
+durationPhases.add(Duration.ofMinutes(4))
+durationPhases.add(Duration.ofMinutes(36))
+durationPhases.add(Duration.ofMinutes(41))
+durationPhases.add(Duration.ofMinutes(104))
+
+var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh)
+
+```
diff --git a/bundles/org.openhab.binding.energidataservice/pom.xml b/bundles/org.openhab.binding.energidataservice/pom.xml
new file mode 100644
index 0000000000000..4e4d39fddfb21
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.0.0-SNAPSHOT
+
+
+ org.openhab.binding.energidataservice
+
+ openHAB Add-ons :: Bundles :: Energi Data Service Binding
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..69ed1af643ffc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.energidataservice/${project.version}
+
+
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
new file mode 100644
index 0000000000000..c55282dd94efb
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java
@@ -0,0 +1,254 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.energidataservice.internal.api.ChargeType;
+import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
+import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
+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.DatahubPricelistRecords;
+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;
+import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
+import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.osgi.framework.FrameworkUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ApiController} is responsible for interacting with Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ApiController {
+ private static final String ENDPOINT = "https://api.energidataservice.dk/";
+ private static final String DATASET_PATH = "dataset/";
+
+ private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices";
+ private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist";
+
+ private static final String FILTER_KEY_PRICE_AREA = "PriceArea";
+ private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType";
+ private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode";
+ private static final String FILTER_KEY_GLN_NUMBER = "GLN_Number";
+ private static final String FILTER_KEY_NOTE = "Note";
+
+ private static final String HEADER_REMAINING_CALLS = "RemainingCalls";
+ private static final String HEADER_TOTAL_CALLS = "TotalCalls";
+
+ private final Logger logger = LoggerFactory.getLogger(ApiController.class);
+ private final Gson gson = new GsonBuilder() //
+ .registerTypeAdapter(Instant.class, new InstantDeserializer()) //
+ .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) //
+ .create();
+ private final HttpClient httpClient;
+ private final TimeZoneProvider timeZoneProvider;
+ private final String userAgent;
+
+ public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
+ this.httpClient = httpClient;
+ this.timeZoneProvider = timeZoneProvider;
+ userAgent = "openHAB " + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
+ }
+
+ /**
+ * Retrieve spot 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 hour start and price in requested currency.
+ * @throws InterruptedException
+ * @throws DataServiceException
+ */
+ public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start,
+ Map properties) throws InterruptedException, DataServiceException {
+ if (!SUPPORTED_CURRENCIES.contains(currency)) {
+ throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
+ }
+
+ Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES)
+ .param("start", start.toString()) //
+ .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") //
+ .param("columns", "HourUTC,SpotPrice" + currency) //
+ .agent(userAgent) //
+ .method(HttpMethod.GET);
+
+ logger.trace("GET request for {}", request.getURI());
+
+ try {
+ ContentResponse response = request.send();
+
+ updatePropertiesFromResponse(response, properties);
+
+ int status = response.getStatus();
+ if (!HttpStatus.isSuccess(status)) {
+ throw new DataServiceException("The request failed with HTTP error " + status, status);
+ }
+ String responseContent = response.getContentAsString();
+ if (responseContent.isEmpty()) {
+ throw new DataServiceException("Empty response");
+ }
+ logger.trace("Response content: '{}'", responseContent);
+
+ ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class);
+ if (records == null) {
+ throw new DataServiceException("Error parsing response");
+ }
+
+ if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) {
+ throw new DataServiceException("No records");
+ }
+
+ return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(ElspotpriceRecord[]::new);
+ } catch (JsonSyntaxException e) {
+ throw new DataServiceException("Error parsing response", e);
+ } catch (TimeoutException | ExecutionException e) {
+ throw new DataServiceException(e);
+ }
+ }
+
+ private void updatePropertiesFromResponse(ContentResponse response, Map properties) {
+ HttpFields headers = response.getHeaders();
+ String remainingCalls = headers.get(HEADER_REMAINING_CALLS);
+ if (remainingCalls != null) {
+ properties.put(PROPERTY_REMAINING_CALLS, remainingCalls);
+ }
+ String totalCalls = headers.get(HEADER_TOTAL_CALLS);
+ if (totalCalls != null) {
+ properties.put(PROPERTY_TOTAL_CALLS, totalCalls);
+ }
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
+ properties.put(PROPERTY_LAST_CALL, LocalDateTime.now(timeZoneProvider.getTimeZone()).format(formatter));
+ }
+
+ /**
+ * Retrieve datahub pricelists for requested GLN and charge type/charge type code.
+ *
+ * @param globalLocationNumber Global Location Number of the Charge Owner
+ * @param chargeType Charge type (Subscription, Fee or Tariff).
+ * @param tariffFilter Tariff filter (charge type codes and notes).
+ * @param properties Map of properties which will be updated with metadata from headers
+ * @return Price list for requested GLN and note.
+ * @throws InterruptedException
+ * @throws DataServiceException
+ */
+ public Collection getDatahubPriceLists(GlobalLocationNumber globalLocationNumber,
+ ChargeType chargeType, DatahubTariffFilter tariffFilter, Map properties)
+ throws InterruptedException, DataServiceException {
+ String columns = "ValidFrom,ValidTo,ChargeTypeCode";
+ for (int i = 1; i < 25; i++) {
+ columns += ",Price" + i;
+ }
+
+ Map> filterMap = new HashMap<>(Map.of( //
+ FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), //
+ FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString())));
+
+ Collection chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings();
+ if (!chargeTypeCodes.isEmpty()) {
+ filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes);
+ }
+
+ Collection notes = tariffFilter.getNotes();
+ if (!notes.isEmpty()) {
+ filterMap.put(FILTER_KEY_NOTE, notes);
+ }
+
+ Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST)
+ .param("filter", mapToFilter(filterMap)) //
+ .param("columns", columns) //
+ .agent(userAgent) //
+ .method(HttpMethod.GET);
+
+ DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter();
+ if (!dateQueryParameter.isEmpty()) {
+ request = request.param("start", dateQueryParameter.toString());
+ }
+
+ logger.trace("GET request for {}", request.getURI());
+
+ try {
+ ContentResponse response = request.send();
+
+ updatePropertiesFromResponse(response, properties);
+
+ int status = response.getStatus();
+ if (!HttpStatus.isSuccess(status)) {
+ throw new DataServiceException("The request failed with HTTP error " + status, status);
+ }
+ String responseContent = response.getContentAsString();
+ if (responseContent.isEmpty()) {
+ throw new DataServiceException("Empty response");
+ }
+ logger.trace("Response content: '{}'", responseContent);
+
+ DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class);
+ if (records == null) {
+ throw new DataServiceException("Error parsing response");
+ }
+
+ if (records.limit() > 0 && records.limit() < records.total()) {
+ logger.warn("{} price list records available, but only {} returned.", records.total(), records.limit());
+ }
+
+ if (Objects.isNull(records.records())) {
+ return List.of();
+ }
+
+ return Arrays.stream(records.records()).filter(Objects::nonNull).toList();
+ } catch (JsonSyntaxException e) {
+ throw new DataServiceException("Error parsing response", e);
+ } catch (TimeoutException | ExecutionException e) {
+ throw new DataServiceException(e);
+ }
+ }
+
+ private String mapToFilter(Map> map) {
+ return "{" + map.entrySet().stream().map(
+ e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]")
+ .collect(Collectors.joining(",")) + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java
new file mode 100644
index 0000000000000..d4673e1afa62e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java
@@ -0,0 +1,443 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
+import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
+
+/**
+ * The {@link CacheManager} is responsible for maintaining a cache of received
+ * data from Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class CacheManager {
+
+ public final static int NUMBER_OF_HISTORIC_HOURS = 12;
+ public final static int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS;
+ public final static int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS;
+
+ private final Clock clock;
+ private final PriceListParser priceListParser = new PriceListParser();
+
+ private Collection netTariffRecords = new ArrayList<>();
+ private Collection systemTariffRecords = new ArrayList<>();
+ private Collection electricityTaxRecords = new ArrayList<>();
+ private Collection transmissionNetTariffRecords = new ArrayList<>();
+
+ private Map spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
+ private Map netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+ private Map systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+ private Map electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+ private Map transmissionNetTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+
+ public CacheManager() {
+ this(Clock.systemDefaultZone());
+ }
+
+ public CacheManager(Clock clock) {
+ this.clock = clock.withZone(NORD_POOL_TIMEZONE);
+ }
+
+ /**
+ * Clear all cached data.
+ */
+ public void clear() {
+ netTariffRecords.clear();
+ systemTariffRecords.clear();
+ electricityTaxRecords.clear();
+ transmissionNetTariffRecords.clear();
+
+ spotPriceMap.clear();
+ netTariffMap.clear();
+ systemTariffMap.clear();
+ electricityTaxMap.clear();
+ transmissionNetTariffMap.clear();
+ }
+
+ /**
+ * Convert and cache the supplied {@link ElspotpriceRecord}s.
+ *
+ * @param records The records as received from Energi Data Service.
+ * @param currency The currency in which the records were requested.
+ */
+ public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) {
+ boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency);
+ for (ElspotpriceRecord record : records) {
+ spotPriceMap.put(record.hour(),
+ (isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000)));
+ }
+ cleanup();
+ }
+
+ /**
+ * Replace current "raw"/unprocessed net tariff records in cache.
+ * Map of hourly tariffs will be updated automatically.
+ *
+ * @param records to cache
+ */
+ public void putNetTariffs(Collection records) {
+ putDatahubRecords(netTariffRecords, records);
+ updateNetTariffs();
+ }
+
+ /**
+ * Replace current "raw"/unprocessed system tariff records in cache.
+ * Map of hourly tariffs will be updated automatically.
+ *
+ * @param records to cache
+ */
+ public void putSystemTariffs(Collection records) {
+ putDatahubRecords(systemTariffRecords, records);
+ updateSystemTariffs();
+ }
+
+ /**
+ * Replace current "raw"/unprocessed electricity tax records in cache.
+ * Map of hourly taxes will be updated automatically.
+ *
+ * @param records to cache
+ */
+ public void putElectricityTaxes(Collection records) {
+ putDatahubRecords(electricityTaxRecords, records);
+ updateElectricityTaxes();
+ }
+
+ /**
+ * Replace current "raw"/unprocessed transmission net tariff records in cache.
+ * Map of hourly tariffs will be updated automatically.
+ *
+ * @param records to cache
+ */
+ public void putTransmissionNetTariffs(Collection records) {
+ putDatahubRecords(transmissionNetTariffRecords, records);
+ updateTransmissionNetTariffs();
+ }
+
+ private void putDatahubRecords(Collection destination,
+ Collection source) {
+ LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE))
+ .minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
+
+ destination.clear();
+ destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
+ }
+
+ /**
+ * Update map of hourly net tariffs from internal cache.
+ */
+ public void updateNetTariffs() {
+ netTariffMap = priceListParser.toHourly(netTariffRecords);
+ cleanup();
+ }
+
+ /**
+ * Update map of system tariffs from internal cache.
+ */
+ public void updateSystemTariffs() {
+ systemTariffMap = priceListParser.toHourly(systemTariffRecords);
+ cleanup();
+ }
+
+ /**
+ * Update map of electricity taxes from internal cache.
+ */
+ public void updateElectricityTaxes() {
+ electricityTaxMap = priceListParser.toHourly(electricityTaxRecords);
+ cleanup();
+ }
+
+ /**
+ * Update map of hourly transmission net tariffs from internal cache.
+ */
+ public void updateTransmissionNetTariffs() {
+ transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords);
+ cleanup();
+ }
+
+ /**
+ * Get current spot price.
+ *
+ * @return spot price currently valid
+ */
+ public @Nullable BigDecimal getSpotPrice() {
+ return getSpotPrice(Instant.now(clock));
+ }
+
+ /**
+ * Get spot price valid at provided instant.
+ *
+ * @param time {@link Instant} for which to get the spot price
+ * @return spot price at given time or null if not available
+ */
+ public @Nullable BigDecimal getSpotPrice(Instant time) {
+ return spotPriceMap.get(getHourStart(time));
+ }
+
+ /**
+ * Get map of all cached spot prices.
+ *
+ * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
+ */
+ public Map getSpotPrices() {
+ return new HashMap(spotPriceMap);
+ }
+
+ /**
+ * Get current net tariff.
+ *
+ * @return net tariff currently valid
+ */
+ public @Nullable BigDecimal getNetTariff() {
+ return getNetTariff(Instant.now(clock));
+ }
+
+ /**
+ * Get net tariff valid at provided instant.
+ *
+ * @param time {@link Instant} for which to get the net tariff
+ * @return net tariff at given time or null if not available
+ */
+ public @Nullable BigDecimal getNetTariff(Instant time) {
+ return netTariffMap.get(getHourStart(time));
+ }
+
+ /**
+ * Get map of all cached net tariffs.
+ *
+ * @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
+ */
+ public Map getNetTariffs() {
+ return new HashMap(netTariffMap);
+ }
+
+ /**
+ * Get current system tariff.
+ *
+ * @return system tariff currently valid
+ */
+ public @Nullable BigDecimal getSystemTariff() {
+ return getSystemTariff(Instant.now(clock));
+ }
+
+ /**
+ * Get system tariff valid at provided instant.
+ *
+ * @param time {@link Instant} for which to get the system tariff
+ * @return system tariff at given time or null if not available
+ */
+ public @Nullable BigDecimal getSystemTariff(Instant time) {
+ return systemTariffMap.get(getHourStart(time));
+ }
+
+ /**
+ * Get map of all cached system tariffs.
+ *
+ * @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
+ */
+ public Map getSystemTariffs() {
+ return new HashMap(systemTariffMap);
+ }
+
+ /**
+ * Get current electricity tax.
+ *
+ * @return electricity tax currently valid
+ */
+ public @Nullable BigDecimal getElectricityTax() {
+ return getElectricityTax(Instant.now(clock));
+ }
+
+ /**
+ * Get electricity tax valid at provided instant.
+ *
+ * @param time {@link Instant} for which to get the electricity tax
+ * @return electricity tax at given time or null if not available
+ */
+ public @Nullable BigDecimal getElectricityTax(Instant time) {
+ return electricityTaxMap.get(getHourStart(time));
+ }
+
+ /**
+ * Get map of all cached electricity taxes.
+ *
+ * @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
+ */
+ public Map getElectricityTaxes() {
+ return new HashMap(electricityTaxMap);
+ }
+
+ /**
+ * Get current transmission net tariff.
+ *
+ * @return transmission net tariff currently valid
+ */
+ public @Nullable BigDecimal getTransmissionNetTariff() {
+ return getTransmissionNetTariff(Instant.now(clock));
+ }
+
+ /**
+ * Get transmission net tariff valid at provided instant.
+ *
+ * @param time {@link Instant} for which to get the transmission net tariff
+ * @return transmission net tariff at given time or null if not available
+ */
+ public @Nullable BigDecimal getTransmissionNetTariff(Instant time) {
+ return transmissionNetTariffMap.get(getHourStart(time));
+ }
+
+ /**
+ * Get map of all cached transmission net tariffs.
+ *
+ * @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
+ */
+ public Map getTransmissionNetTariffs() {
+ return new HashMap(transmissionNetTariffMap);
+ }
+
+ /**
+ * Get number of future spot prices including current hour.
+ *
+ * @return number of future spot prices
+ */
+ public long getNumberOfFutureSpotPrices() {
+ Instant currentHourStart = getCurrentHourStart();
+
+ return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
+ }
+
+ /**
+ * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
+ *
+ * @return true if historic spot prices are cached
+ */
+ public boolean areHistoricSpotPricesCached() {
+ return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
+ }
+
+ /**
+ * Check if all current spot prices are cached taking into consideration that next day's spot prices
+ * should be available at 13:00 CET.
+ *
+ * @return true if spot prices are fully cached
+ */
+ public boolean areSpotPricesFullyCached() {
+ Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant();
+ LocalTime now = LocalTime.now(clock);
+ if (now.isAfter(DAILY_REFRESH_TIME_CET)) {
+ end = end.plus(24, ChronoUnit.HOURS);
+ }
+
+ return arePricesCached(spotPriceMap, end);
+ }
+
+ private boolean arePricesCached(Map priceMap, Instant end) {
+ for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
+ ChronoUnit.HOURS)) {
+ if (priceMap.get(hourStart) == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if we have "raw" net tariff records cached which are valid tomorrow.
+ *
+ * @return true if net tariff records for tomorrow are cached
+ */
+ public boolean areNetTariffsValidTomorrow() {
+ return isValidNextDay(netTariffRecords);
+ }
+
+ /**
+ * Check if we have "raw" system tariff records cached which are valid tomorrow.
+ *
+ * @return true if system tariff records for tomorrow are cached
+ */
+ public boolean areSystemTariffsValidTomorrow() {
+ return isValidNextDay(systemTariffRecords);
+ }
+
+ /**
+ * Check if we have "raw" electricity tax records cached which are valid tomorrow.
+ *
+ * @return true if electricity tax records for tomorrow are cached
+ */
+ public boolean areElectricityTaxesValidTomorrow() {
+ return isValidNextDay(electricityTaxRecords);
+ }
+
+ /**
+ * Check if we have "raw" transmission net tariff records cached which are valid tomorrow.
+ *
+ * @return true if transmission net tariff records for tomorrow are cached
+ */
+ public boolean areTransmissionNetTariffsValidTomorrow() {
+ return isValidNextDay(transmissionNetTariffRecords);
+ }
+
+ /**
+ * Remove historic prices.
+ */
+ public void cleanup() {
+ Instant firstHourStart = getFirstHourStart();
+
+ spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
+ netTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
+ systemTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
+ electricityTaxMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
+ transmissionNetTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
+ }
+
+ private boolean isValidNextDay(Collection records) {
+ LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
+ .truncatedTo(ChronoUnit.HOURS);
+ LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS);
+
+ return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
+ }
+
+ private Instant getCurrentHourStart() {
+ return getHourStart(Instant.now(clock));
+ }
+
+ private Instant getFirstHourStart() {
+ return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
+ }
+
+ private Instant getHourStart(Instant instant) {
+ return instant.truncatedTo(ChronoUnit.HOURS);
+ }
+}
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
new file mode 100644
index 0000000000000..38358f0c03343
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Currency;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link EnergiDataServiceBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class EnergiDataServiceBindingConstants {
+
+ private static final String BINDING_ID = "energidataservice";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service");
+
+ // List of all Channel Group ids
+ public static final String CHANNEL_GROUP_ELECTRICITY = "electricity";
+
+ // List of all Channel ids
+ public static final String CHANNEL_CURRENT_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY
+ + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSpotPrice";
+ public static final String CHANNEL_CURRENT_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY
+ + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentNetTariff";
+ public static final String CHANNEL_CURRENT_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY
+ + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSystemTariff";
+ public static final String CHANNEL_CURRENT_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY
+ + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentElectricityTax";
+ public static final String CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY
+ + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentTransmissionNetTariff";
+ public static final String CHANNEL_HOURLY_PRICES = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+ + "hourlyPrices";
+
+ public static final Set ELECTRICITY_CHANNELS = Set.of(CHANNEL_CURRENT_SPOT_PRICE,
+ CHANNEL_CURRENT_NET_TARIFF, CHANNEL_CURRENT_SYSTEM_TARIFF, CHANNEL_CURRENT_ELECTRICITY_TAX,
+ CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, CHANNEL_HOURLY_PRICES);
+
+ // List of all properties
+ public static final String PROPERTY_REMAINING_CALLS = "remainingCalls";
+ public static final String PROPERTY_TOTAL_CALLS = "totalCalls";
+ public static final String PROPERTY_LAST_CALL = "lastCall";
+ public static final String PROPERTY_NEXT_CALL = "nextCall";
+
+ // List of supported currencies
+ public static final Currency CURRENCY_DKK = Currency.getInstance("DKK");
+ public static final Currency CURRENCY_EUR = Currency.getInstance("EUR");
+
+ public static final Set SUPPORTED_CURRENCIES = Set.of(CURRENCY_DKK, CURRENCY_EUR);
+
+ // Time-zone of Datahub
+ public static final ZoneId DATAHUB_TIMEZONE = ZoneId.of("CET");
+ public static final ZoneId NORD_POOL_TIMEZONE = ZoneId.of("CET");
+
+ // 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 String PROPERTY_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java
new file mode 100644
index 0000000000000..3524f90b1ca9f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides calculations based on price maps.
+ * This is the current stage of evolution.
+ * Ideally this binding would simply provide data in a well-defined format for
+ * openHAB core. Operations on this data could then be implemented in core.
+ * This way there would be a unified interface from rules, and the calculations
+ * could be reused between different data providers (bindings).
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class PriceCalculator {
+
+ private final Logger logger = LoggerFactory.getLogger(PriceCalculator.class);
+
+ private final Map priceMap;
+
+ public PriceCalculator(Map priceMap) {
+ this.priceMap = priceMap;
+ }
+
+ /**
+ * Calculate cheapest period from list of durations with specified amount of energy
+ * used per phase.
+ *
+ * @param earliestStart Earliest allowed start time.
+ * @param latestEnd Latest allowed end time.
+ * @param totalDuration Total duration to fit.
+ * @param durationPhases List of {@link Duration}'s representing different phases of using power.
+ * @param energyUsedPerPhase Amount of energy used per phase.
+ *
+ * @return Map containing resulting values
+ */
+ public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration,
+ List durationPhases, QuantityType energyUsedPerPhase) throws MissingPriceException {
+ QuantityType energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR);
+ if (energyInWattHour == null) {
+ throw new IllegalArgumentException(
+ "Invalid unit " + energyUsedPerPhase.getUnit() + ", expected energy unit");
+ }
+ // watts = (kWh × 1,000) ÷ hrs
+ int numerator = energyInWattHour.intValue() * 3600;
+ List> consumptionPhases = new ArrayList<>();
+ Duration remainingDuration = totalDuration;
+ for (Duration phase : durationPhases) {
+ consumptionPhases.add(QuantityType.valueOf(numerator / phase.getSeconds(), Units.WATT));
+ remainingDuration = remainingDuration.minus(phase);
+ }
+ if (remainingDuration.isNegative()) {
+ throw new IllegalArgumentException("totalDuration must be equal to or greater than sum of phases");
+ }
+ if (!remainingDuration.isZero()) {
+ List durationsWithTermination = new ArrayList<>(durationPhases);
+ durationsWithTermination.add(remainingDuration);
+ consumptionPhases.add(QuantityType.valueOf(0, Units.WATT));
+ return calculateCheapestPeriod(earliestStart, latestEnd, durationsWithTermination, consumptionPhases);
+ }
+ return calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, consumptionPhases);
+ }
+
+ /**
+ * Calculate cheapest period from duration with linear power usage.
+ *
+ * @param earliestStart Earliest allowed start time.
+ * @param latestEnd Latest allowed end time.
+ * @param duration Duration to fit.
+ * @param power Power consumption for the duration of time.
+ *
+ * @return Map containing resulting values
+ */
+ public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration,
+ QuantityType power) throws MissingPriceException {
+ return calculateCheapestPeriod(earliestStart, latestEnd, List.of(duration), List.of(power));
+ }
+
+ /**
+ * Calculate cheapest period from list of durations with corresponding list of consumption
+ * per duration.
+ *
+ * @param earliestStart Earliest allowed start time.
+ * @param latestEnd Latest allowed end time.
+ * @param durationPhases List of {@link Duration}'s representing different phases of using power.
+ * @param consumptionPhases Corresponding List of power consumption for the duration of time.
+ *
+ * @return Map containing resulting values
+ */
+ public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd,
+ List durationPhases, List> consumptionPhases) throws MissingPriceException {
+ if (durationPhases.size() != consumptionPhases.size()) {
+ throw new IllegalArgumentException("Number of phases do not match");
+ }
+ Map result = new HashMap<>();
+ Duration totalDuration = durationPhases.stream().reduce(Duration.ZERO, Duration::plus);
+ Instant calculationStart = earliestStart;
+ Instant calculationEnd = earliestStart.plus(totalDuration);
+ BigDecimal lowestPrice = BigDecimal.valueOf(Double.MAX_VALUE);
+ BigDecimal highestPrice = BigDecimal.ZERO;
+ Instant cheapestStart = Instant.MIN;
+ Instant mostExpensiveStart = Instant.MIN;
+
+ while (calculationEnd.compareTo(latestEnd) <= 0) {
+ BigDecimal currentPrice = BigDecimal.ZERO;
+ Duration minDurationUntilNextHour = Duration.ofHours(1);
+ Instant atomStart = calculationStart;
+
+ int i = 0;
+ for (Duration atomDuration : durationPhases) {
+ Instant atomEnd = atomStart.plus(atomDuration);
+ Instant hourStart = atomStart.truncatedTo(ChronoUnit.HOURS);
+ Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
+
+ // Get next intersection with hourly rate change.
+ Duration durationUntilNextHour = Duration.between(atomStart, hourEnd);
+ if (durationUntilNextHour.compareTo(minDurationUntilNextHour) < 0) {
+ minDurationUntilNextHour = durationUntilNextHour;
+ }
+
+ BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, consumptionPhases.get(i));
+ currentPrice = currentPrice.add(atomPrice);
+ atomStart = atomEnd;
+
+ i++;
+ }
+
+ if (currentPrice.compareTo(lowestPrice) < 0) {
+ lowestPrice = currentPrice;
+ cheapestStart = calculationStart;
+ }
+ if (currentPrice.compareTo(highestPrice) > 0) {
+ highestPrice = currentPrice;
+ mostExpensiveStart = calculationStart;
+ }
+
+ // Now fast forward to next hourly rate intersection.
+ calculationStart = calculationStart.plus(minDurationUntilNextHour);
+ calculationEnd = calculationStart.plus(totalDuration);
+ }
+
+ if (!cheapestStart.equals(Instant.MIN)) {
+ result.put("CheapestStart", cheapestStart);
+ result.put("LowestPrice", lowestPrice);
+ result.put("MostExpensiveStart", mostExpensiveStart);
+ result.put("HighestPrice", highestPrice);
+ }
+
+ return result;
+ }
+
+ /**
+ * Calculate total price from 'start' to 'end' given linear power consumption.
+ *
+ * @param start Start time
+ * @param end End time
+ * @param power The current power consumption.
+ */
+ public BigDecimal calculatePrice(Instant start, Instant end, QuantityType power)
+ throws MissingPriceException {
+ QuantityType quantityInWatt = power.toUnit(Units.WATT);
+ if (quantityInWatt == null) {
+ throw new IllegalArgumentException("Invalid unit " + power.getUnit() + ", expected power unit");
+ }
+ BigDecimal watt = new BigDecimal(quantityInWatt.intValue());
+ if (watt.equals(BigDecimal.ZERO)) {
+ return BigDecimal.ZERO;
+ }
+
+ Instant current = start;
+ BigDecimal result = BigDecimal.ZERO;
+ while (current.isBefore(end)) {
+ Instant hourStart = current.truncatedTo(ChronoUnit.HOURS);
+ Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
+
+ BigDecimal currentPrice = priceMap.get(hourStart);
+ if (currentPrice == null) {
+ throw new MissingPriceException("Price missing at " + hourStart.toString());
+ }
+
+ Instant currentStart = hourStart;
+ if (start.isAfter(hourStart)) {
+ currentStart = start;
+ }
+ Instant currentEnd = hourEnd;
+ if (end.isBefore(hourEnd)) {
+ currentEnd = end;
+ }
+
+ // E(kWh) = P(W) × t(hr) / 1000
+ Duration duration = Duration.between(currentStart, currentEnd);
+ BigDecimal contribution = currentPrice.multiply(watt).multiply(
+ new BigDecimal(duration.getSeconds()).divide(new BigDecimal(3600000), 9, RoundingMode.HALF_UP));
+ result = result.add(contribution);
+ logger.trace("Period {}-{}: {} @ {}", currentStart, currentEnd, contribution, currentPrice);
+
+ current = hourEnd;
+ }
+
+ return result;
+ }
+}
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
new file mode 100644
index 0000000000000..5f4bedc1a3f31
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
+
+/**
+ * Parses results from {@link DatahubPricelistRecords} into map of hourly tariffs.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class PriceListParser {
+
+ private final Clock clock;
+
+ public PriceListParser() {
+ this(Clock.system(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ }
+
+ public PriceListParser(Clock clock) {
+ this.clock = clock;
+ }
+
+ public Map toHourly(Collection records) {
+ Map totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
+ records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> {
+ Map currentMap = toHourly(records, chargeTypeCode);
+ for (Entry current : currentMap.entrySet()) {
+ BigDecimal total = totalMap.get(current.getKey());
+ if (total == null) {
+ total = BigDecimal.ZERO;
+ }
+ totalMap.put(current.getKey(), total.add(current.getValue()));
+ }
+ });
+
+ return totalMap;
+ }
+
+ public Map toHourly(Collection records, String chargeTypeCode) {
+ 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();
+ for (Instant hourStart = firstHourStart; hourStart
+ .isBefore(lastHourStart); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) {
+ LocalDateTime localDateTime = hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
+ .toLocalDateTime();
+ if (localDateTime.compareTo(previousValidFrom) < 0 || localDateTime.compareTo(previousValidTo) >= 0) {
+ DatahubPricelistRecord priceList = getTariffs(records, localDateTime, chargeTypeCode);
+ if (priceList != null) {
+ tariffs = priceList.getTariffMap();
+ previousValidFrom = priceList.validFrom();
+ previousValidTo = priceList.validTo();
+ } else {
+ tariffs = Map.of();
+ }
+ }
+
+ LocalTime localTime = LocalTime
+ .of(hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE).getHour(), 0);
+ BigDecimal tariff = tariffs.get(localTime);
+ if (tariff != null) {
+ tariffMap.put(hourStart, tariff);
+ }
+ }
+
+ return tariffMap;
+ }
+
+ private @Nullable DatahubPricelistRecord getTariffs(Collection records,
+ LocalDateTime localDateTime, String chargeTypeCode) {
+ return records.stream()
+ .filter(record -> localDateTime.compareTo(record.validFrom()) >= 0
+ && localDateTime.compareTo(record.validTo()) < 0
+ && record.chargeTypeCode().equals(chargeTypeCode))
+ .findFirst().orElse(null);
+ }
+}
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
new file mode 100644
index 0000000000000..a39897deb7137
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java
@@ -0,0 +1,381 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.action;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.PriceCalculator;
+import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
+import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@ThingActionsScope(name = "energidataservice")
+@NonNullByDefault
+public class EnergiDataServiceActions implements ThingActions {
+
+ private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
+
+ private @Nullable EnergiDataServiceHandler handler;
+
+ private enum PriceElement {
+ SPOT_PRICE("spotprice"),
+ NET_TARIFF("nettariff"),
+ SYSTEM_TARIFF("systemtariff"),
+ ELECTRICITY_TAX("electricitytax"),
+ TRANSMISSION_NET_TARIFF("transmissionnettariff");
+
+ private static final Map NAME_MAP = Stream.of(values())
+ .collect(Collectors.toMap(PriceElement::toString, Function.identity()));
+
+ private String name;
+
+ private PriceElement(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static PriceElement fromString(final String name) {
+ PriceElement 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;
+ }
+ }
+
+ @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
+ public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices() {
+ return getPrices(Arrays.stream(PriceElement.values()).collect(Collectors.toSet()));
+ }
+
+ @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
+ public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices(
+ @ActionInput(name = "priceElements", label = "@text/action.get-prices.priceElements.label", description = "@text/action.get-prices.priceElements.description") @Nullable String priceElements) {
+ if (priceElements == null) {
+ logger.warn("Argument 'priceElements' is null");
+ return Map.of();
+ }
+
+ Set priceElementsSet;
+ try {
+ priceElementsSet = new HashSet(
+ Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList());
+ } catch (IllegalArgumentException e) {
+ logger.warn("{}", e.getMessage());
+ return Map.of();
+ }
+
+ return getPrices(priceElementsSet);
+ }
+
+ @RuleAction(label = "@text/action.calculate-price.label", description = "@text/action.calculate-price.description")
+ public @ActionOutput(name = "price", type = "java.math.BigDecimal") BigDecimal calculatePrice(
+ @ActionInput(name = "start", type = "java.time.Instant") Instant start,
+ @ActionInput(name = "end", type = "java.time.Instant") Instant end,
+ @ActionInput(name = "power", type = "QuantityType") QuantityType power) {
+ PriceCalculator priceCalculator = new PriceCalculator(getPrices());
+
+ try {
+ return priceCalculator.calculatePrice(start, end, power);
+ } catch (MissingPriceException e) {
+ logger.warn("{}", e.getMessage());
+ return BigDecimal.ZERO;
+ }
+ }
+
+ @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
+ public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod(
+ @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
+ @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
+ @ActionInput(name = "duration", type = "java.time.Duration") Duration duration) {
+ PriceCalculator priceCalculator = new PriceCalculator(getPrices());
+
+ try {
+ Map intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
+ duration, QuantityType.valueOf(1000, Units.WATT));
+
+ // Create new result with stripped price information.
+ Map result = new HashMap<>();
+ Object value = intermediateResult.get("CheapestStart");
+ if (value != null) {
+ result.put("CheapestStart", value);
+ }
+ value = intermediateResult.get("MostExpensiveStart");
+ if (value != null) {
+ result.put("MostExpensiveStart", value);
+ }
+ return result;
+ } catch (MissingPriceException | IllegalArgumentException e) {
+ logger.warn("{}", e.getMessage());
+ return Map.of();
+ }
+ }
+
+ @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
+ public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod(
+ @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
+ @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
+ @ActionInput(name = "duration", type = "java.time.Duration") Duration duration,
+ @ActionInput(name = "power", type = "QuantityType") QuantityType power) {
+ PriceCalculator priceCalculator = new PriceCalculator(getPrices());
+
+ try {
+ return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
+ } catch (MissingPriceException | IllegalArgumentException e) {
+ logger.warn("{}", e.getMessage());
+ return Map.of();
+ }
+ }
+
+ @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
+ public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod(
+ @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
+ @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
+ @ActionInput(name = "totalDuration", type = "java.time.Duration") Duration totalDuration,
+ @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases,
+ @ActionInput(name = "energyUsedPerPhase", type = "QuantityType") QuantityType energyUsedPerPhase) {
+ PriceCalculator priceCalculator = new PriceCalculator(getPrices());
+
+ try {
+ return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
+ energyUsedPerPhase);
+ } catch (MissingPriceException | IllegalArgumentException e) {
+ logger.warn("{}", e.getMessage());
+ return Map.of();
+ }
+ }
+
+ @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
+ public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod(
+ @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
+ @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
+ @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases,
+ @ActionInput(name = "powerPhases", type = "java.util.List>") List> powerPhases) {
+ if (durationPhases.size() != powerPhases.size()) {
+ logger.warn("Number of duration phases ({}) is different from number of consumption phases ({})",
+ durationPhases.size(), powerPhases.size());
+ return Map.of();
+ }
+ PriceCalculator priceCalculator = new PriceCalculator(getPrices());
+
+ try {
+ return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
+ } catch (MissingPriceException | IllegalArgumentException e) {
+ logger.warn("{}", e.getMessage());
+ return Map.of();
+ }
+ }
+
+ private Map getPrices(Set priceElements) {
+ EnergiDataServiceHandler handler = this.handler;
+ if (handler == null) {
+ logger.warn("EnergiDataServiceActions ThingHandler is null.");
+ return Map.of();
+ }
+
+ Map prices;
+ boolean spotPricesRequired;
+ if (priceElements.contains(PriceElement.SPOT_PRICE)) {
+ if (priceElements.size() > 1 && !handler.getCurrency().equals(CURRENCY_DKK)) {
+ logger.warn("Cannot calculate sum when spot price currency is {}", handler.getCurrency());
+ return Map.of();
+ }
+ prices = handler.getSpotPrices();
+ spotPricesRequired = true;
+ } else {
+ spotPricesRequired = false;
+ prices = new HashMap<>();
+ }
+
+ if (priceElements.contains(PriceElement.NET_TARIFF)) {
+ Map netTariffMap = handler.getNetTariffs();
+ mergeMaps(prices, netTariffMap, !spotPricesRequired);
+ }
+
+ if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
+ Map systemTariffMap = handler.getSystemTariffs();
+ mergeMaps(prices, systemTariffMap, !spotPricesRequired);
+ }
+
+ if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
+ Map electricityTaxMap = handler.getElectricityTaxes();
+ mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
+ }
+
+ if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) {
+ Map transmissionNetTariffMap = handler.getTransmissionNetTariffs();
+ mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired);
+ }
+
+ return prices;
+ }
+
+ private void mergeMaps(Map destinationMap, Map sourceMap,
+ boolean createNew) {
+ for (Entry source : sourceMap.entrySet()) {
+ Instant key = source.getKey();
+ BigDecimal sourceValue = source.getValue();
+ BigDecimal destinationValue = destinationMap.get(key);
+ if (destinationValue != null) {
+ destinationMap.put(key, sourceValue.add(destinationValue));
+ } else if (createNew) {
+ destinationMap.put(key, sourceValue);
+ }
+ }
+ }
+
+ /**
+ * Static get prices method for DSL rule compatibility.
+ *
+ * @param actions
+ * @param priceElements Comma-separated list of price elements to include in prices.
+ * @return Map of prices
+ */
+ public static Map getPrices(@Nullable ThingActions actions, @Nullable String priceElements) {
+ if (actions instanceof EnergiDataServiceActions) {
+ if (priceElements != null && !priceElements.isBlank()) {
+ return ((EnergiDataServiceActions) actions).getPrices(priceElements);
+ } else {
+ return ((EnergiDataServiceActions) actions).getPrices();
+ }
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ /**
+ * Static get prices method for DSL rule compatibility.
+ *
+ * @param actions
+ * @param start Start time
+ * @param end End time
+ * @param power Constant power consumption
+ * @return Map of prices
+ */
+ public static BigDecimal calculatePrice(@Nullable ThingActions actions, @Nullable Instant start,
+ @Nullable Instant end, @Nullable QuantityType power) {
+ if (start == null || end == null || power == null) {
+ return BigDecimal.ZERO;
+ }
+ if (actions instanceof EnergiDataServiceActions) {
+ return ((EnergiDataServiceActions) actions).calculatePrice(start, end, power);
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ public static Map calculateCheapestPeriod(@Nullable ThingActions actions,
+ @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration) {
+ if (actions instanceof EnergiDataServiceActions) {
+ if (earliestStart == null || latestEnd == null || duration == null) {
+ return Map.of();
+ }
+ return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration);
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ public static Map calculateCheapestPeriod(@Nullable ThingActions actions,
+ @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration,
+ @Nullable QuantityType power) {
+ if (actions instanceof EnergiDataServiceActions) {
+ if (earliestStart == null || latestEnd == null || duration == null || power == null) {
+ return Map.of();
+ }
+ return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration,
+ power);
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ public static Map calculateCheapestPeriod(@Nullable ThingActions actions,
+ @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration,
+ @Nullable List durationPhases, @Nullable QuantityType energyUsedPerPhase) {
+ if (actions instanceof EnergiDataServiceActions) {
+ if (earliestStart == null || latestEnd == null || totalDuration == null || durationPhases == null
+ || energyUsedPerPhase == null) {
+ return Map.of();
+ }
+ return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, totalDuration,
+ durationPhases, energyUsedPerPhase);
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ public static Map calculateCheapestPeriod(@Nullable ThingActions actions,
+ @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List durationPhases,
+ @Nullable List> powerPhases) {
+ if (actions instanceof EnergiDataServiceActions) {
+ if (earliestStart == null || latestEnd == null || durationPhases == null || powerPhases == null) {
+ return Map.of();
+ }
+ return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd,
+ durationPhases, powerPhases);
+ } else {
+ throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
+ }
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof EnergiDataServiceHandler) {
+ this.handler = (EnergiDataServiceHandler) handler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java
new file mode 100644
index 0000000000000..19d242b740915
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Charge type for DatahubPricelist dataset.
+ * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}}
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public enum ChargeType {
+ Subscription("D01"),
+ Fee("D02"),
+ Tariff("D03");
+
+ private final String code;
+
+ ChargeType(String code) {
+ this.code = code;
+ }
+
+ @Override
+ public String toString() {
+ return code;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java
new file mode 100644
index 0000000000000..b9228a17215bc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Charge type code for DatahubPricelist dataset.
+ * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}}
+ * These codes are defined by the individual grid companies.
+ * For example, N1 uses "CD" for "Nettarif C" and "CD R" for "Rabat på nettarif N1 A/S".
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ChargeTypeCode {
+
+ private static final int MAX_LENGTH = 20;
+
+ private final String code;
+
+ public ChargeTypeCode(String code) {
+ if (code.length() > MAX_LENGTH) {
+ throw new IllegalArgumentException("Maximum length exceeded: " + code);
+ }
+ this.code = code;
+ }
+
+ @Override
+ public String toString() {
+ return code;
+ }
+
+ public static ChargeTypeCode of(String code) {
+ return new ChargeTypeCode(code);
+ }
+}
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
new file mode 100644
index 0000000000000..ecbec4a8bb94c
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import java.util.Collection;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Filter for the {@link DatahubPricelist} dataset.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DatahubTariffFilter {
+
+ private final Set chargeTypeCodes;
+ private final Set notes;
+ private final DateQueryParameter dateQueryParameter;
+
+ public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) {
+ this(filter.chargeTypeCodes, filter.notes, dateQueryParameter);
+ }
+
+ public DatahubTariffFilter(Set chargeTypeCodes, Set notes) {
+ this(chargeTypeCodes, notes, DateQueryParameter.EMPTY);
+ }
+
+ public DatahubTariffFilter(Set chargeTypeCodes, Set notes,
+ DateQueryParameter dateQueryParameter) {
+ this.chargeTypeCodes = chargeTypeCodes;
+ this.notes = notes;
+ this.dateQueryParameter = dateQueryParameter;
+ }
+
+ public Collection getChargeTypeCodesAsStrings() {
+ return chargeTypeCodes.stream().map(c -> c.toString()).toList();
+ }
+
+ public Collection getNotes() {
+ return notes;
+ }
+
+ public DateQueryParameter getDateQueryParameter() {
+ return dateQueryParameter;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java
new file mode 100644
index 0000000000000..070c57cef01b9
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Factory for creating a {@link DatahubTariffFilter} for a specific Grid Company GLN.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DatahubTariffFilterFactory {
+
+ private static final String GLN_CERIUS = "5790000705184";
+ private static final String GLN_DINEL = "5790000610099";
+ private static final String GLN_ELEKTRUS = "5790000836239";
+ private static final String GLN_ELINORD = "5790001095277";
+ private static final String GLN_ELNET_MIDT = "5790001100520";
+ private static final String GLN_ELNET_KONGERSLEV = "5790002502699";
+ private static final String GLN_FLOW_ELNET = "5790000392551";
+ private static final String GLN_HAMMEL_ELFORSYNING_NET = "5790001090166";
+ private static final String GLN_HURUP_ELVAERK_NET = "5790000610839";
+ private static final String GLN_IKAST_E1_NET = "5790000682102";
+ private static final String GLN_KONSTANT = "5790000704842";
+ private static final String GLN_L_NET = "5790001090111";
+ private static final String GLN_N1 = "5790001089030";
+ private static final String GLN_N1_RANDERS = "5790000681372";
+ private static final String GLN_NETSELSKABET_ELVAERK = "5790000681075";
+ private static final String GLN_NKE_ELNET = "5790001088231";
+ private static final String GLN_NORD_ENERGI_NET = "5790000610877";
+ private static final String GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET = "5790000395620";
+ private static final String GLN_RADIUS = "5790000705689";
+ private static final String GLN_RAH_NET = "5790000681327";
+ private static final String GLN_RAVDEX = "5790000836727";
+ private static final String GLN_TARM_ELVAERK_NET = "5790000706419";
+ private static final String GLN_TREFOR_EL_NET = "5790000392261";
+ private static final String GLN_TREFOR_EL_NET_OEST = "5790000706686";
+ private static final String GLN_VEKSEL = "5790001088217";
+ private static final String GLN_VORES_ELNET = "5790000610976";
+ private static final String GLN_ZEANET = "5790001089375";
+
+ private static final String NOTE_NET_TARIFF = "Nettarif";
+ private static final String NOTE_NET_TARIFF_C = NOTE_NET_TARIFF + " C";
+ private static final String NOTE_NET_TARIFF_C_HOUR = NOTE_NET_TARIFF_C + " time";
+ private static final String NOTE_SYSTEM_TARIFF = "Systemtarif";
+ private static final String NOTE_ELECTRICITY_TAX = "Elafgift";
+ private static final String NOTE_TRANSMISSION_NET_TARIFF = "Transmissions nettarif";
+
+ public static final LocalDate N1_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
+ public static final LocalDate RADIUS_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
+ public static final LocalDate KONSTANT_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
+
+ public static DatahubTariffFilter getNetTariffByGLN(String globalLocationNumber) {
+ switch (globalLocationNumber) {
+ case GLN_CERIUS:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("30TR_C_ET")), Set.of(NOTE_NET_TARIFF_C_HOUR));
+ case GLN_DINEL:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TCL>100_02")), Set.of(NOTE_NET_TARIFF_C_HOUR));
+ case GLN_ELEKTRUS:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("6000091")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_ELINORD:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("43300")),
+ Set.of("Transportbetaling, eget net C"));
+ case GLN_ELNET_MIDT:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("T3002")), Set.of(NOTE_NET_TARIFF_C),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_ELNET_KONGERSLEV:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("K_22100")), Set.of(NOTE_NET_TARIFF_C));
+ case GLN_FLOW_ELNET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("FE2 NT-01")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_HAMMEL_ELFORSYNING_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("50001")), Set.of("Overliggende net"),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_HURUP_ELVAERK_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("HEV-NT-01")), Set.of(NOTE_NET_TARIFF));
+ case GLN_IKAST_E1_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("IEV-NT-01"), ChargeTypeCode.of("IEV-NT-11")),
+ Set.of(NOTE_NET_TARIFF_C_HOUR, "Transport - Overordnet net"));
+ case GLN_KONSTANT:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("151-NT01T")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(KONSTANT_CUTOFF_DATE));
+ case GLN_L_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("4010")), Set.of(NOTE_NET_TARIFF_C_HOUR));
+ case GLN_N1:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD"), ChargeTypeCode.of("CD R")), Set.of(),
+ DateQueryParameter.of(N1_CUTOFF_DATE));
+ case GLN_N1_RANDERS:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD")), Set.of(NOTE_NET_TARIFF_C),
+ DateQueryParameter.of(N1_CUTOFF_DATE));
+ case GLN_NETSELSKABET_ELVAERK:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("0NCFF")), Set.of(NOTE_NET_TARIFF_C + " Flex"),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_NKE_ELNET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("94TR_C_ET")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_NORD_ENERGI_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TA031U200")), Set.of(NOTE_NET_TARIFF_C));
+ case GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("Net C")), Set.of(NOTE_NET_TARIFF_C));
+ case GLN_RADIUS:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("DT_C_01")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(RADIUS_CUTOFF_DATE));
+ case GLN_RAH_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("RAH-C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_RAVDEX:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_TARM_ELVAERK_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TEV-NT-01")), Set.of(NOTE_NET_TARIFF_C));
+ case GLN_TREFOR_EL_NET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_TREFOR_EL_NET_OEST:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("46")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_VEKSEL:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-10")),
+ Set.of(NOTE_NET_TARIFF_C_HOUR + " NT-10"));
+ case GLN_VORES_ELNET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TNT1009")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ case GLN_ZEANET:
+ return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("43110")), Set.of(NOTE_NET_TARIFF_C_HOUR),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+ default:
+ return new DatahubTariffFilter(Set.of(), Set.of(NOTE_NET_TARIFF_C),
+ DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR));
+ }
+ }
+
+ public static DatahubTariffFilter getSystemTariff() {
+ return new DatahubTariffFilter(Set.of(), Set.of(NOTE_SYSTEM_TARIFF),
+ DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
+ }
+
+ public static DatahubTariffFilter getElectricityTax() {
+ return new DatahubTariffFilter(Set.of(), Set.of(NOTE_ELECTRICITY_TAX),
+ DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
+ }
+
+ public static DatahubTariffFilter getTransmissionNetTariff() {
+ return new DatahubTariffFilter(Set.of(), Set.of(NOTE_TRANSMISSION_NET_TARIFF),
+ DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
+ }
+}
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
new file mode 100644
index 0000000000000..70f996a97c56b
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import java.time.Duration;
+import java.time.LocalDate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This class represents a query parameter of type {@link LocalDate} or a
+ * dynamic date defined as {@link DateQueryParameterType} with an optional offset.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DateQueryParameter {
+
+ public static final DateQueryParameter EMPTY = new DateQueryParameter();
+
+ private @Nullable LocalDate date;
+ private @Nullable Duration offset;
+ private @Nullable DateQueryParameterType dateType;
+
+ private DateQueryParameter() {
+ }
+
+ public DateQueryParameter(LocalDate date) {
+ this.date = date;
+ }
+
+ public DateQueryParameter(DateQueryParameterType dateType, Duration offset) {
+ this.dateType = dateType;
+ this.offset = offset;
+ }
+
+ public DateQueryParameter(DateQueryParameterType dateType) {
+ this.dateType = dateType;
+ }
+
+ @Override
+ public String toString() {
+ LocalDate date = this.date;
+ if (date != null) {
+ return date.toString();
+ }
+ DateQueryParameterType dateType = this.dateType;
+ if (dateType != null) {
+ Duration offset = this.offset;
+ if (offset == null) {
+ return dateType.toString();
+ } else {
+ return dateType.toString()
+ + (offset.isNegative() ? "-" + offset.abs().toString() : "+" + offset.toString());
+ }
+ }
+ return "null";
+ }
+
+ public boolean isEmpty() {
+ return this == EMPTY;
+ }
+
+ public static DateQueryParameter of(LocalDate localDate) {
+ return new DateQueryParameter(localDate);
+ }
+
+ public static DateQueryParameter of(DateQueryParameterType dateType, Duration offset) {
+ if (offset.isZero()) {
+ return new DateQueryParameter(dateType);
+ } else {
+ return new DateQueryParameter(dateType, offset);
+ }
+ }
+
+ public static DateQueryParameter of(DateQueryParameterType dateType) {
+ return new DateQueryParameter(dateType);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java
new file mode 100644
index 0000000000000..3d951b27c8c15
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This class represents a dynamic date to be used in a query.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public enum DateQueryParameterType {
+ NOW("now"),
+ UTC_NOW("utcnow"),
+ START_OF_DAY("StartOfDay"),
+ START_OF_MONTH("StartOfMonth"),
+ START_OF_YEAR("StartOfYear");
+
+ private final String name;
+
+ DateQueryParameterType(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java
new file mode 100644
index 0000000000000..4baa897b042bc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Global Location Number.
+ * See {@link https://www.gs1.org/standards/id-keys/gln}}
+ * The Global Location Number (GLN) can be used by companies to identify their locations.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class GlobalLocationNumber {
+
+ public static final GlobalLocationNumber EMPTY = new GlobalLocationNumber("");
+
+ private static final int MAX_LENGTH = 13;
+
+ private final String gln;
+
+ public GlobalLocationNumber(String gln) {
+ if (gln.length() > MAX_LENGTH) {
+ throw new IllegalArgumentException("Maximum length exceeded: " + gln);
+ }
+ this.gln = gln;
+ }
+
+ @Override
+ public String toString() {
+ return gln;
+ }
+
+ public boolean isEmpty() {
+ return this == EMPTY;
+ }
+
+ public boolean isValid() {
+ if (gln.length() != 13) {
+ return false;
+ }
+
+ int checksum = 0;
+ for (int i = 13 - 2; i >= 0; i--) {
+ int digit = Character.getNumericValue(gln.charAt(i));
+ checksum += (i % 2 == 0 ? digit : digit * 3);
+ }
+ int controlDigit = 10 - (checksum % 10);
+ if (controlDigit == 10) {
+ controlDigit = 0;
+ }
+
+ return controlDigit == Character.getNumericValue(gln.charAt(13 - 1));
+ }
+
+ public static GlobalLocationNumber of(String gln) {
+ if (gln.isBlank()) {
+ return EMPTY;
+ }
+ return new GlobalLocationNumber(gln);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java
new file mode 100644
index 0000000000000..4cbae11721db0
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.dto;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Record as part of {@link DatahubPricelistRecords} from Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public record DatahubPricelistRecord(@SerializedName("ValidFrom") LocalDateTime validFrom,
+ @SerializedName("ValidTo") LocalDateTime validTo, @SerializedName("ChargeTypeCode") String chargeTypeCode,
+ @SerializedName("Price1") BigDecimal price1, @SerializedName("Price2") BigDecimal price2,
+ @SerializedName("Price3") BigDecimal price3, @SerializedName("Price4") BigDecimal price4,
+ @SerializedName("Price5") BigDecimal price5, @SerializedName("Price6") BigDecimal price6,
+ @SerializedName("Price7") BigDecimal price7, @SerializedName("Price8") BigDecimal price8,
+ @SerializedName("Price9") BigDecimal price9, @SerializedName("Price10") BigDecimal price10,
+ @SerializedName("Price11") BigDecimal price11, @SerializedName("Price12") BigDecimal price12,
+ @SerializedName("Price13") BigDecimal price13, @SerializedName("Price14") BigDecimal price14,
+ @SerializedName("Price15") BigDecimal price15, @SerializedName("Price16") BigDecimal price16,
+ @SerializedName("Price17") BigDecimal price17, @SerializedName("Price18") BigDecimal price18,
+ @SerializedName("Price19") BigDecimal price19, @SerializedName("Price20") BigDecimal price20,
+ @SerializedName("Price21") BigDecimal price21, @SerializedName("Price22") BigDecimal price22,
+ @SerializedName("Price23") BigDecimal price23, @SerializedName("Price24") BigDecimal price24) {
+
+ @Override
+ public LocalDateTime validTo() {
+ return Objects.isNull(validTo) ? LocalDateTime.MAX : validTo;
+ }
+
+ @Override
+ public BigDecimal price2() {
+ return Objects.requireNonNullElse(price2, price1());
+ }
+
+ @Override
+ public BigDecimal price3() {
+ return Objects.requireNonNullElse(price3, price1());
+ }
+
+ @Override
+ public BigDecimal price4() {
+ return Objects.requireNonNullElse(price4, price1());
+ }
+
+ @Override
+ public BigDecimal price5() {
+ return Objects.requireNonNullElse(price5, price1());
+ }
+
+ @Override
+ public BigDecimal price6() {
+ return Objects.requireNonNullElse(price6, price1());
+ }
+
+ @Override
+ public BigDecimal price7() {
+ return Objects.requireNonNullElse(price7, price1());
+ }
+
+ @Override
+ public BigDecimal price8() {
+ return Objects.requireNonNullElse(price8, price1());
+ }
+
+ @Override
+ public BigDecimal price9() {
+ return Objects.requireNonNullElse(price9, price1());
+ }
+
+ @Override
+ public BigDecimal price10() {
+ return Objects.requireNonNullElse(price10, price1());
+ }
+
+ @Override
+ public BigDecimal price11() {
+ return Objects.requireNonNullElse(price11, price1());
+ }
+
+ @Override
+ public BigDecimal price12() {
+ return Objects.requireNonNullElse(price12, price1());
+ }
+
+ @Override
+ public BigDecimal price13() {
+ return Objects.requireNonNullElse(price13, price1());
+ }
+
+ @Override
+ public BigDecimal price14() {
+ return Objects.requireNonNullElse(price14, price1());
+ }
+
+ @Override
+ public BigDecimal price15() {
+ return Objects.requireNonNullElse(price15, price1());
+ }
+
+ @Override
+ public BigDecimal price16() {
+ return Objects.requireNonNullElse(price16, price1());
+ }
+
+ @Override
+ public BigDecimal price17() {
+ return Objects.requireNonNullElse(price17, price1());
+ }
+
+ @Override
+ public BigDecimal price18() {
+ return Objects.requireNonNullElse(price18, price1());
+ }
+
+ @Override
+ public BigDecimal price19() {
+ return Objects.requireNonNullElse(price19, price1());
+ }
+
+ @Override
+ public BigDecimal price20() {
+ return Objects.requireNonNullElse(price20, price1());
+ }
+
+ @Override
+ public BigDecimal price21() {
+ return Objects.requireNonNullElse(price21, price1());
+ }
+
+ @Override
+ public BigDecimal price22() {
+ return Objects.requireNonNullElse(price22, price1());
+ }
+
+ @Override
+ public BigDecimal price23() {
+ return Objects.requireNonNullElse(price23, price1());
+ }
+
+ @Override
+ public BigDecimal price24() {
+ return Objects.requireNonNullElse(price24, price1());
+ }
+
+ /**
+ * Get {@link Map} of tariffs with hour start as key.
+ *
+ * @return map with hourly tariffs
+ */
+ public Map getTariffMap() {
+ Map tariffMap = new HashMap<>();
+
+ tariffMap.put(LocalTime.of(0, 0), price1());
+ tariffMap.put(LocalTime.of(1, 0), price2());
+ tariffMap.put(LocalTime.of(2, 0), price3());
+ tariffMap.put(LocalTime.of(3, 0), price4());
+ tariffMap.put(LocalTime.of(4, 0), price5());
+ tariffMap.put(LocalTime.of(5, 0), price6());
+ tariffMap.put(LocalTime.of(6, 0), price7());
+ tariffMap.put(LocalTime.of(7, 0), price8());
+ tariffMap.put(LocalTime.of(8, 0), price9());
+ tariffMap.put(LocalTime.of(9, 0), price10());
+ tariffMap.put(LocalTime.of(10, 0), price11());
+ tariffMap.put(LocalTime.of(11, 0), price12());
+ tariffMap.put(LocalTime.of(12, 0), price13());
+ tariffMap.put(LocalTime.of(13, 0), price14());
+ tariffMap.put(LocalTime.of(14, 0), price15());
+ tariffMap.put(LocalTime.of(15, 0), price16());
+ tariffMap.put(LocalTime.of(16, 0), price17());
+ tariffMap.put(LocalTime.of(17, 0), price18());
+ tariffMap.put(LocalTime.of(18, 0), price19());
+ tariffMap.put(LocalTime.of(19, 0), price20());
+ tariffMap.put(LocalTime.of(20, 0), price21());
+ tariffMap.put(LocalTime.of(21, 0), price22());
+ tariffMap.put(LocalTime.of(22, 0), price23());
+ tariffMap.put(LocalTime.of(23, 0), price24());
+
+ return tariffMap;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java
new file mode 100644
index 0000000000000..4aa6c28a53f65
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Received {@link DatahubPricelistRecords} from Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public record DatahubPricelistRecords(int total, String filters, int limit, String dataset,
+ DatahubPricelistRecord[] records) {
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java
new file mode 100644
index 0000000000000..a657471c995cc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.dto;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Record as part of {@link ElspotpriceRecords} from Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public record ElspotpriceRecord(@SerializedName("HourUTC") Instant hour,
+ @SerializedName("SpotPriceDKK") BigDecimal spotPriceDKK,
+ @SerializedName("SpotPriceEUR") BigDecimal spotPriceEUR) {
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java
new file mode 100644
index 0000000000000..87c8c2d71183f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Received {@link ElspotpriceRecords} from Energi Data Service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public record ElspotpriceRecords(int total, String filters, String dataset, ElspotpriceRecord[] records) {
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java
new file mode 100644
index 0000000000000..22c4ea66cbe3e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.serialization;
+
+import java.lang.reflect.Type;
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link InstantDeserializer} converts a formatted UTC string to {@link Instant}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class InstantDeserializer implements JsonDeserializer {
+
+ @Override
+ public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+ throws JsonParseException {
+ String content = element.getAsString();
+ // When writing this, the format of the provided UTC strings lacks the trailing 'Z'.
+ // In case this would be fixed in the future, gracefully support both with and without this.
+ return Instant.parse(content.endsWith("Z") ? content : content + "Z");
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java
new file mode 100644
index 0000000000000..5b51db3d30b82
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.serialization;
+
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link LocalDateDeserializer} converts a formatted string to {@link LocalDate}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class LocalDateDeserializer implements JsonDeserializer {
+
+ @Override
+ public @Nullable LocalDate deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+ throws JsonParseException {
+ return LocalDate.parse(element.getAsString().substring(0, 10));
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java
new file mode 100644
index 0000000000000..47851c05e0df3
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api.serialization;
+
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link LocalDateTimeDeserializer} converts a formatted string to {@link LocalDateTime}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class LocalDateTimeDeserializer implements JsonDeserializer {
+
+ @Override
+ public @Nullable LocalDateTime deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+ throws JsonParseException {
+ return LocalDateTime.parse(element.getAsString());
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java
new file mode 100644
index 0000000000000..d5dd072b0d0d0
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.config;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
+import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
+import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
+
+/**
+ * The {@link DatahubPriceConfiguration} class contains fields mapping channel configuration parameters.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DatahubPriceConfiguration extends PriceConfiguration {
+
+ /**
+ * Comma-separated list of charge type codes, e.g. "CD,CD R".
+ */
+ public String chargeTypeCodes = "";
+
+ /**
+ * Comma-separated list of notes, e.g. "Nettarif C".
+ */
+ public String notes = "";
+
+ /**
+ * Query start date parameter expressed as either yyyy-mm-dd or one of StartOfDay, StartOfMonth or StartOfYear.
+ */
+ public String start = "";
+
+ /**
+ * Check if any filter values are provided.
+ *
+ * @return true if either charge type codes, notes or query start date is provided.
+ */
+ public boolean hasAnyFilterOverrides() {
+ return !chargeTypeCodes.isBlank() || !notes.isBlank() || !start.isBlank();
+ }
+
+ /**
+ * Get parsed set of charge type codes from comma-separated string.
+ *
+ * @return Set of charge type codes.
+ */
+ public Set getChargeTypeCodes() {
+ return chargeTypeCodes.isBlank() ? new HashSet<>()
+ : new HashSet(
+ Arrays.stream(chargeTypeCodes.split(",")).map(ChargeTypeCode::new).toList());
+ }
+
+ /**
+ * Get parsed set of notes from comma-separated string.
+ *
+ * @return Set of notes.
+ */
+ public Set getNotes() {
+ return notes.isBlank() ? new HashSet<>() : new HashSet(Arrays.asList(notes.split(",")));
+ }
+
+ /**
+ * Get query start parameter.
+ *
+ * @return null if invalid, otherwise an initialized {@link DateQueryParameter}.
+ */
+ public @Nullable DateQueryParameter getStart() {
+ if (start.isBlank()) {
+ return DateQueryParameter.EMPTY;
+ }
+ if (start.equals(DateQueryParameterType.START_OF_DAY.toString())) {
+ return DateQueryParameter.of(DateQueryParameterType.START_OF_DAY);
+ }
+ if (start.equals(DateQueryParameterType.START_OF_MONTH.toString())) {
+ return DateQueryParameter.of(DateQueryParameterType.START_OF_MONTH);
+ }
+ if (start.equals(DateQueryParameterType.START_OF_YEAR.toString())) {
+ return DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR);
+ }
+ try {
+ return DateQueryParameter.of(LocalDate.parse(start));
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java
new file mode 100644
index 0000000000000..70cf0b45df7d9
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.config;
+
+import java.util.Currency;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants;
+import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
+
+/**
+ * The {@link EnergiDataServiceConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class EnergiDataServiceConfiguration {
+
+ /**
+ * Price area (DK1 = West of the Great Belt, DK2 = East of the Great Belt).
+ */
+ public String priceArea = "";
+
+ /**
+ * Currency code for the prices.
+ */
+ public String currencyCode = EnergiDataServiceBindingConstants.CURRENCY_DKK.getCurrencyCode();
+
+ /**
+ * Global Location Number of the Grid Company.
+ */
+ public String gridCompanyGLN = "";
+
+ /**
+ * Global Location Number of Energinet.
+ */
+ public String energinetGLN = "5790000432752";
+
+ /**
+ * Get {@link Currency} representing the configured currency code.
+ *
+ * @return Currency instance
+ */
+ public Currency getCurrency() {
+ return Currency.getInstance(currencyCode);
+ }
+
+ public GlobalLocationNumber getGridCompanyGLN() {
+ return GlobalLocationNumber.of(gridCompanyGLN);
+ }
+
+ public GlobalLocationNumber getEnerginetGLN() {
+ return GlobalLocationNumber.of(energinetGLN);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java
new file mode 100644
index 0000000000000..10628ba174bd2
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PriceConfiguration} class defines common configuration parameters for price
+ * channels.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class PriceConfiguration {
+
+ public static final String INCLUDE_VAT = "includeVAT";
+
+ /**
+ * Add VAT to amount based on regional settings.
+ */
+ public boolean includeVAT = false;
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java
new file mode 100644
index 0000000000000..ab2c522e49a84
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link DataServiceException} is a generic Energi Data Service exception thrown in case
+ * of communication failure or unexpected response. It is intended to be derived by
+ * specialized exceptions.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DataServiceException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+ private int httpStatus = 0;
+
+ public DataServiceException(String message) {
+ super(message);
+ }
+
+ public DataServiceException(Throwable cause) {
+ super(cause);
+ }
+
+ public DataServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public DataServiceException(String message, int httpStatus) {
+ super(message);
+ this.httpStatus = httpStatus;
+ }
+
+ public int getHttpStatus() {
+ return httpStatus;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java
new file mode 100644
index 0000000000000..95814c24a0dd5
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link MissingPriceException} is thrown when there are no prices
+ * available in the requested interval, e.g. when performing a calculation.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class MissingPriceException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public MissingPriceException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java
new file mode 100644
index 0000000000000..4212d80857d17
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.factory;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link EnergiDataServiceHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.energidataservice", service = ThingHandlerFactory.class)
+public class EnergiDataServiceHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE);
+
+ private final HttpClient httpClient;
+ private final LocaleProvider localeProvider;
+ private final TimeZoneProvider timeZoneProvider;
+
+ @Activate
+ public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference LocaleProvider localeProvider, final @Reference TimeZoneProvider timeZoneProvider,
+ ComponentContext componentContext) {
+ super.activate(componentContext);
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.localeProvider = localeProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_SERVICE.equals(thingTypeUID)) {
+ return new EnergiDataServiceHandler(thing, httpClient, localeProvider, timeZoneProvider);
+ }
+
+ return null;
+ }
+}
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
new file mode 100644
index 0000000000000..6d8bf4e1dbead
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java
@@ -0,0 +1,561 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.handler;
+
+import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.Currency;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.energidataservice.internal.ApiController;
+import org.openhab.binding.energidataservice.internal.CacheManager;
+import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions;
+import org.openhab.binding.energidataservice.internal.api.ChargeType;
+import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
+import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
+import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory;
+import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
+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.ElspotpriceRecord;
+import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration;
+import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration;
+import org.openhab.binding.energidataservice.internal.config.PriceConfiguration;
+import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
+import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link EnergiDataServiceHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class EnergiDataServiceHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class);
+ private final LocaleProvider localeProvider;
+ private final TimeZoneProvider timeZoneProvider;
+ private final ApiController apiController;
+ private final CacheManager cacheManager;
+ private final Gson gson = new Gson();
+
+ private EnergiDataServiceConfiguration config;
+ private RetryStrategy retryPolicy = RetryPolicyFactory.initial();
+ private @Nullable ScheduledFuture> refreshFuture;
+ private @Nullable ScheduledFuture> priceUpdateFuture;
+
+ private record Price(String hourStart, BigDecimal spotPrice, String spotPriceCurrency,
+ @Nullable BigDecimal netTariff, @Nullable BigDecimal systemTariff, @Nullable BigDecimal electricityTax,
+ @Nullable BigDecimal transmissionNetTariff) {
+ }
+
+ public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, LocaleProvider localeProvider,
+ TimeZoneProvider timeZoneProvider) {
+ super(thing);
+ this.localeProvider = localeProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ this.apiController = new ApiController(httpClient, timeZoneProvider);
+ this.cacheManager = new CacheManager();
+
+ // Default configuration
+ this.config = new EnergiDataServiceConfiguration();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (!(command instanceof RefreshType)) {
+ return;
+ }
+
+ if (ELECTRICITY_CHANNELS.contains(channelUID.getId())) {
+ refreshElectricityPrices();
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(EnergiDataServiceConfiguration.class);
+
+ if (config.priceArea.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.no-price-area");
+ return;
+ }
+ GlobalLocationNumber gln = config.getGridCompanyGLN();
+ if (!gln.isEmpty() && !gln.isValid()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.invalid-grid-company-gln");
+ return;
+ }
+ gln = config.getEnerginetGLN();
+ if (!gln.isEmpty() && !gln.isValid()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.invalid-energinet-gln");
+ return;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ refreshFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> refreshFuture = this.refreshFuture;
+ if (refreshFuture != null) {
+ refreshFuture.cancel(true);
+ this.refreshFuture = null;
+ }
+ ScheduledFuture> priceUpdateFuture = this.priceUpdateFuture;
+ if (priceUpdateFuture != null) {
+ priceUpdateFuture.cancel(true);
+ this.priceUpdateFuture = null;
+ }
+
+ cacheManager.clear();
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(EnergiDataServiceActions.class);
+ }
+
+ private void refreshElectricityPrices() {
+ RetryStrategy retryPolicy;
+ try {
+ if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ downloadSpotPrices();
+ }
+
+ if (isLinked(CHANNEL_CURRENT_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ downloadNetTariffs();
+ }
+
+ if (isLinked(CHANNEL_CURRENT_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ downloadSystemTariffs();
+ }
+
+ if (isLinked(CHANNEL_CURRENT_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ downloadElectricityTaxes();
+ }
+
+ if (isLinked(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ downloadTransmissionNetTariffs();
+ }
+
+ updateStatus(ThingStatus.ONLINE);
+ updatePrices();
+
+ if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) {
+ if (cacheManager.getNumberOfFutureSpotPrices() < 13) {
+ retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing(DAILY_REFRESH_TIME_CET,
+ NORD_POOL_TIMEZONE);
+ } else {
+ retryPolicy = RetryPolicyFactory.atFixedTime(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE);
+ }
+ } else {
+ retryPolicy = RetryPolicyFactory.atFixedTime(LocalTime.MIDNIGHT, timeZoneProvider.getTimeZone());
+ }
+ } catch (DataServiceException e) {
+ if (e.getHttpStatus() != 0) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ HttpStatus.getCode(e.getHttpStatus()).getMessage());
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ if (e.getCause() != null) {
+ logger.debug("Error retrieving prices", e);
+ }
+ retryPolicy = RetryPolicyFactory.fromThrowable(e);
+ } catch (InterruptedException e) {
+ logger.debug("Refresh job interrupted");
+ Thread.currentThread().interrupt();
+ return;
+ }
+
+ rescheduleRefreshJob(retryPolicy);
+ }
+
+ private void downloadSpotPrices() throws InterruptedException, DataServiceException {
+ if (cacheManager.areSpotPricesFullyCached()) {
+ logger.debug("Cached spot prices still valid, skipping download.");
+ return;
+ }
+ DateQueryParameter start;
+ if (cacheManager.areHistoricSpotPricesCached()) {
+ start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW);
+ } else {
+ start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
+ Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS));
+ }
+ Map properties = editProperties();
+ ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(),
+ start, properties);
+ cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency());
+ updateProperties(properties);
+ }
+
+ private void downloadNetTariffs() throws InterruptedException, DataServiceException {
+ if (config.getGridCompanyGLN().isEmpty()) {
+ return;
+ }
+ if (cacheManager.areNetTariffsValidTomorrow()) {
+ logger.debug("Cached net tariffs still valid, skipping download.");
+ cacheManager.updateNetTariffs();
+ } else {
+ cacheManager.putNetTariffs(downloadPriceLists(config.getGridCompanyGLN(), getNetTariffFilter()));
+ }
+ }
+
+ private void downloadSystemTariffs() throws InterruptedException, DataServiceException {
+ GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
+ if (globalLocationNumber.isEmpty()) {
+ return;
+ }
+ if (cacheManager.areSystemTariffsValidTomorrow()) {
+ logger.debug("Cached system tariffs still valid, skipping download.");
+ cacheManager.updateSystemTariffs();
+ } else {
+ cacheManager.putSystemTariffs(
+ downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getSystemTariff()));
+ }
+ }
+
+ private void downloadElectricityTaxes() throws InterruptedException, DataServiceException {
+ GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
+ if (globalLocationNumber.isEmpty()) {
+ return;
+ }
+ if (cacheManager.areElectricityTaxesValidTomorrow()) {
+ logger.debug("Cached electricity taxes still valid, skipping download.");
+ cacheManager.updateElectricityTaxes();
+ } else {
+ cacheManager.putElectricityTaxes(
+ downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getElectricityTax()));
+ }
+ }
+
+ private void downloadTransmissionNetTariffs() throws InterruptedException, DataServiceException {
+ GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
+ if (globalLocationNumber.isEmpty()) {
+ return;
+ }
+ if (cacheManager.areTransmissionNetTariffsValidTomorrow()) {
+ logger.debug("Cached transmission net tariffs still valid, skipping download.");
+ cacheManager.updateTransmissionNetTariffs();
+ } else {
+ cacheManager.putTransmissionNetTariffs(
+ downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getTransmissionNetTariff()));
+ }
+ }
+
+ private Collection downloadPriceLists(GlobalLocationNumber globalLocationNumber,
+ DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
+ Map properties = editProperties();
+ Collection records = apiController.getDatahubPriceLists(globalLocationNumber,
+ ChargeType.Tariff, filter, properties);
+ updateProperties(properties);
+
+ return records;
+ }
+
+ private DatahubTariffFilter getNetTariffFilter() {
+ Channel channel = getThing().getChannel(CHANNEL_CURRENT_NET_TARIFF);
+ if (channel == null) {
+ return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
+ }
+
+ DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration()
+ .as(DatahubPriceConfiguration.class);
+
+ if (!datahubPriceConfiguration.hasAnyFilterOverrides()) {
+ return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
+ }
+
+ DateQueryParameter start = datahubPriceConfiguration.getStart();
+ if (start == null) {
+ logger.warn("Invalid channel configuration parameter 'start': {}", datahubPriceConfiguration.start);
+ return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
+ }
+
+ Set chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes();
+ Set notes = datahubPriceConfiguration.getNotes();
+ if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) {
+ // Completely override filter.
+ return new DatahubTariffFilter(chargeTypeCodes, notes, start);
+ } else {
+ // Only override start date in pre-configured filter.
+ return new DatahubTariffFilter(DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN), start);
+ }
+ }
+
+ private void updatePrices() {
+ cacheManager.cleanup();
+
+ updateCurrentSpotPrice();
+ updateCurrentTariff(CHANNEL_CURRENT_NET_TARIFF, cacheManager.getNetTariff());
+ updateCurrentTariff(CHANNEL_CURRENT_SYSTEM_TARIFF, cacheManager.getSystemTariff());
+ updateCurrentTariff(CHANNEL_CURRENT_ELECTRICITY_TAX, cacheManager.getElectricityTax());
+ updateCurrentTariff(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, cacheManager.getTransmissionNetTariff());
+ updateHourlyPrices();
+
+ reschedulePriceUpdateJob();
+ }
+
+ private void updateCurrentSpotPrice() {
+ if (!isLinked(CHANNEL_CURRENT_SPOT_PRICE)) {
+ return;
+ }
+ BigDecimal price = getVATAdjustedPrice(cacheManager.getSpotPrice(), CHANNEL_CURRENT_SPOT_PRICE);
+ updateState(CHANNEL_CURRENT_SPOT_PRICE, price != null ? new DecimalType(price) : UnDefType.UNDEF);
+ }
+
+ private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) {
+ if (!isLinked(channelId)) {
+ return;
+ }
+ BigDecimal price = getVATAdjustedPrice(tariff, channelId);
+ updateState(channelId, price != null ? new DecimalType(price) : UnDefType.UNDEF);
+ }
+
+ private @Nullable BigDecimal getVATAdjustedPrice(@Nullable BigDecimal price, String channelId) {
+ if (price == null) {
+ return price;
+ }
+ Channel channel = getThing().getChannel(channelId);
+ if (channel == null) {
+ return price;
+ }
+ Object obj = channel.getConfiguration().get(PriceConfiguration.INCLUDE_VAT);
+ if (obj == null) {
+ return price;
+ }
+ Boolean includeVAT = (Boolean) obj;
+ if (includeVAT) {
+ return price.multiply(getVATPercentageFactor());
+ }
+ return price;
+ }
+
+ private BigDecimal getVATPercentageFactor() {
+ String country = localeProvider.getLocale().getCountry();
+ switch (country) {
+ case "DK":
+ case "NO":
+ case "SE":
+ return new BigDecimal("1.25");
+ case "DE":
+ return new BigDecimal("1.19");
+ default:
+ logger.debug("No VAT rate for country {}", country);
+ return BigDecimal.ONE;
+ }
+ }
+
+ private void updateHourlyPrices() {
+ if (!isLinked(CHANNEL_HOURLY_PRICES)) {
+ return;
+ }
+ Map spotPriceMap = cacheManager.getSpotPrices();
+ Price[] targetPrices = new Price[spotPriceMap.size()];
+ List> sourcePrices = spotPriceMap.entrySet().stream()
+ .sorted(Map.Entry. comparingByKey()).toList();
+
+ int i = 0;
+ for (Entry sourcePrice : sourcePrices) {
+ Instant hourStart = sourcePrice.getKey();
+ BigDecimal netTariff = cacheManager.getNetTariff(hourStart);
+ BigDecimal systemTariff = cacheManager.getSystemTariff(hourStart);
+ BigDecimal electricityTax = cacheManager.getElectricityTax(hourStart);
+ BigDecimal transmissionNetTariff = cacheManager.getTransmissionNetTariff(hourStart);
+ targetPrices[i++] = new Price(hourStart.toString(), sourcePrice.getValue(), config.currencyCode, netTariff,
+ systemTariff, electricityTax, transmissionNetTariff);
+ }
+ updateState(CHANNEL_HOURLY_PRICES, new StringType(gson.toJson(targetPrices)));
+ }
+
+ /**
+ * Get the configured {@link Currency} for spot prices.
+ *
+ * @return Spot price currency
+ */
+ public Currency getCurrency() {
+ return config.getCurrency();
+ }
+
+ /**
+ * Get cached spot prices or try once to download them if not cached
+ * (usually if no items are linked).
+ *
+ * @return Map of future spot prices
+ */
+ public Map getSpotPrices() {
+ try {
+ downloadSpotPrices();
+ } catch (DataServiceException e) {
+ logger.warn("Error retrieving spot prices");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return cacheManager.getSpotPrices();
+ }
+
+ /**
+ * Get cached net tariffs or try once to download them if not cached
+ * (usually if no items are linked).
+ *
+ * @return Map of future net tariffs
+ */
+ public Map getNetTariffs() {
+ try {
+ downloadNetTariffs();
+ } catch (DataServiceException e) {
+ logger.warn("Error retrieving net tariffs");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return cacheManager.getNetTariffs();
+ }
+
+ /**
+ * Get cached system tariffs or try once to download them if not cached
+ * (usually if no items are linked).
+ *
+ * @return Map of future system tariffs
+ */
+ public Map getSystemTariffs() {
+ try {
+ downloadSystemTariffs();
+ } catch (DataServiceException e) {
+ logger.warn("Error retrieving system tariffs");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return cacheManager.getSystemTariffs();
+ }
+
+ /**
+ * Get cached electricity taxes or try once to download them if not cached
+ * (usually if no items are linked).
+ *
+ * @return Map of future electricity taxes
+ */
+ public Map getElectricityTaxes() {
+ try {
+ downloadElectricityTaxes();
+ } catch (DataServiceException e) {
+ logger.warn("Error retrieving electricity taxes");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return cacheManager.getElectricityTaxes();
+ }
+
+ /**
+ * Return cached transmission net tariffs or try once to download them if not cached
+ * (usually if no items are linked).
+ *
+ * @return Map of future transmissions net tariffs
+ */
+ public Map getTransmissionNetTariffs() {
+ try {
+ downloadTransmissionNetTariffs();
+ } catch (DataServiceException e) {
+ logger.warn("Error retrieving transmission net tariffs");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return cacheManager.getTransmissionNetTariffs();
+ }
+
+ private void reschedulePriceUpdateJob() {
+ ScheduledFuture> priceUpdateJob = this.priceUpdateFuture;
+ if (priceUpdateJob != null) {
+ // Do not interrupt ourselves.
+ priceUpdateJob.cancel(false);
+ this.priceUpdateFuture = null;
+ }
+
+ Instant now = Instant.now();
+ long millisUntilNextClockHour = Duration
+ .between(now, now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS)).toMillis() + 1;
+ this.priceUpdateFuture = scheduler.schedule(this::updatePrices, millisUntilNextClockHour,
+ TimeUnit.MILLISECONDS);
+ logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour);
+ }
+
+ private void rescheduleRefreshJob(RetryStrategy retryPolicy) {
+ // Preserve state of previous retry policy when configuration is the same.
+ if (!retryPolicy.equals(this.retryPolicy)) {
+ this.retryPolicy = retryPolicy;
+ }
+
+ ScheduledFuture> refreshJob = this.refreshFuture;
+
+ long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds();
+ Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh);
+ this.refreshFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh,
+ TimeUnit.SECONDS);
+ logger.debug("Refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh);
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
+ updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone())
+ .truncatedTo(ChronoUnit.SECONDS).format(formatter));
+
+ if (refreshJob != null) {
+ refreshJob.cancel(true);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java
new file mode 100644
index 0000000000000..00f0ab89cc4e8
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.ZoneId;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
+import org.openhab.binding.energidataservice.internal.retry.strategy.ExponentialBackoff;
+import org.openhab.binding.energidataservice.internal.retry.strategy.FixedTime;
+import org.openhab.binding.energidataservice.internal.retry.strategy.Linear;
+
+/**
+ * This factory defines policies for determining appropriate {@link RetryStrategy} based
+ * on scenario.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class RetryPolicyFactory {
+
+ /**
+ * Determine {@link RetryStrategy} from {@link Throwable}.
+ *
+ * @param e thrown exception
+ * @return retry strategy
+ */
+ public static RetryStrategy fromThrowable(Throwable e) {
+ if (e instanceof DataServiceException dse) {
+ switch (dse.getHttpStatus()) {
+ case HttpStatus.TOO_MANY_REQUESTS_429:
+ return new ExponentialBackoff().withMinimum(Duration.ofMinutes(30));
+ default:
+ return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2);
+ }
+ }
+
+ return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2);
+ }
+
+ /**
+ * Default {@link RetryStrategy} with one retry per day.
+ * This is intended as a dummy strategy until replaced by a concrete one.
+ *
+ * @return retry strategy
+ */
+ public static RetryStrategy initial() {
+ return new Linear().withMinimum(Duration.ofDays(1));
+ }
+
+ /**
+ * Determine {@link RetryStrategy} for next expected data publishing.
+ *
+ * @param localTime the time of daily data request in local time-zone
+ * @param zoneId the local time-zone
+ * @return retry strategy
+ */
+ public static RetryStrategy atFixedTime(LocalTime localTime, ZoneId zoneId) {
+ return new FixedTime(localTime, Clock.system(zoneId)).withJitter(1);
+ }
+
+ /**
+ * Determine {@link RetryStrategy} when expected spot price data is missing.
+ *
+ * @param utcTime the time of daily data request in UTC time-zone
+ * @return retry strategy
+ */
+ public static RetryStrategy whenExpectedSpotPriceDataMissing(LocalTime localTime, ZoneId zoneId) {
+ LocalTime now = LocalTime.now(zoneId);
+ if (now.isAfter(localTime)) {
+ return new ExponentialBackoff().withMinimum(Duration.ofMinutes(10)).withJitter(0.2);
+ }
+ return atFixedTime(localTime, zoneId);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java
new file mode 100644
index 0000000000000..eb67ba8300056
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry;
+
+import java.time.Duration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This interface defines a retry strategy for failed network
+ * requests.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public interface RetryStrategy {
+ /**
+ * Get {@link Duration} until next attempt. This will auto-increment number of
+ * attempts, so should only be called once after each failed request.
+ *
+ * @return duration until next attempt according to strategy
+ */
+ Duration getDuration();
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java
new file mode 100644
index 0000000000000..4b510ae22cacc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import java.time.Duration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * This implements a {@link RetryStrategy} for exponential backoff with jitter.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ExponentialBackoff implements RetryStrategy {
+
+ private int attempts = 0;
+ private int factor = 2;
+ private double jitter = 0.0;
+ private Duration minimum = Duration.ofMillis(100);
+ private Duration maximum = Duration.ofHours(6);
+
+ public ExponentialBackoff() {
+ }
+
+ @Override
+ public Duration getDuration() {
+ long minimum = this.minimum.toMillis();
+ long maximum = this.maximum.toMillis();
+ long duration = minimum * (long) Math.pow(this.factor, this.attempts++);
+ if (jitter != 0.0) {
+ double rand = Math.random();
+ if ((((int) Math.floor(rand * 10)) & 1) == 0) {
+ duration += (long) (rand * jitter * duration);
+ } else {
+ duration -= (long) (rand * jitter * duration);
+ }
+ }
+ if (duration < minimum) {
+ duration = minimum;
+ }
+ if (duration > maximum) {
+ duration = maximum;
+ }
+ return Duration.ofMillis(duration);
+ }
+
+ public ExponentialBackoff withFactor(int factor) {
+ this.factor = factor;
+ return this;
+ }
+
+ public ExponentialBackoff withJitter(double jitter) {
+ this.jitter = jitter;
+ return this;
+ }
+
+ public ExponentialBackoff withMinimum(Duration minimum) {
+ this.minimum = minimum;
+ return this;
+ }
+
+ public ExponentialBackoff withMaximum(Duration maximum) {
+ this.maximum = maximum;
+ return this;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ExponentialBackoff)) {
+ return false;
+ }
+ ExponentialBackoff other = (ExponentialBackoff) o;
+
+ return this.factor == other.factor && this.jitter == other.jitter && this.minimum.equals(other.minimum)
+ && this.maximum.equals(other.maximum);
+ }
+
+ @Override
+ public final int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + factor;
+ result = prime * result + (int) jitter * 100;
+ result = prime * result + (int) minimum.toMillis();
+ result = prime * result + (int) maximum.toMillis();
+
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java
new file mode 100644
index 0000000000000..98d78a0485a5f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * This implements a {@link RetryStrategy} for a fixed time.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class FixedTime implements RetryStrategy {
+
+ private final Clock clock;
+
+ private LocalTime localTime;
+ private double jitter = 0.0;
+
+ public FixedTime(LocalTime localTime, Clock clock) {
+ this.localTime = localTime;
+ this.clock = clock;
+ }
+
+ @Override
+ public Duration getDuration() {
+ LocalTime now = LocalTime.now(clock);
+ LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(clock), localTime);
+ if (now.isAfter(localTime)) {
+ localDateTime = localDateTime.plusDays(1);
+ }
+
+ Duration base = Duration.between(LocalDateTime.now(clock), localDateTime);
+ if (jitter == 0.0) {
+ return base;
+ }
+
+ long duration = base.toMillis();
+ double rand = Math.random();
+ duration += (long) (rand * jitter * 1000 * 60);
+
+ return Duration.ofMillis(duration);
+ }
+
+ public FixedTime withJitter(double jitter) {
+ this.jitter = jitter;
+ return this;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof FixedTime)) {
+ return false;
+ }
+ FixedTime other = (FixedTime) o;
+
+ return this.jitter == other.jitter && this.localTime.equals(other.localTime);
+ }
+
+ @Override
+ public final int hashCode() {
+ final int result = 1;
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java
new file mode 100644
index 0000000000000..99bd57a460b7d
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import java.time.Duration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * This implements a {@link RetryStrategy} for linear retry with jitter.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class Linear implements RetryStrategy {
+
+ private double jitter = 0.0;
+ private Duration minimum = Duration.ofMillis(100);
+ private Duration maximum = Duration.ofHours(6);
+
+ public Linear() {
+ }
+
+ @Override
+ public Duration getDuration() {
+ long minimum = this.minimum.toMillis();
+ long maximum = this.maximum.toMillis();
+ long duration = minimum;
+ if (jitter != 0.0) {
+ double rand = Math.random();
+ if ((((int) Math.floor(rand * 10)) & 1) == 0) {
+ duration += (long) (rand * jitter * duration);
+ } else {
+ duration -= (long) (rand * jitter * duration);
+ }
+ }
+ if (duration < minimum) {
+ duration = minimum;
+ }
+ if (duration > maximum) {
+ duration = maximum;
+ }
+ return Duration.ofMillis(duration);
+ }
+
+ public Linear withJitter(double jitter) {
+ this.jitter = jitter;
+ return this;
+ }
+
+ public Linear withMinimum(Duration minimum) {
+ this.minimum = minimum;
+ return this;
+ }
+
+ public Linear withMaximum(Duration maximum) {
+ this.maximum = maximum;
+ return this;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof Linear)) {
+ return false;
+ }
+ Linear other = (Linear) o;
+
+ return this.jitter == other.jitter && this.minimum.equals(other.minimum) && this.maximum.equals(other.maximum);
+ }
+
+ @Override
+ public final int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) jitter * 100;
+ result = prime * result + (int) minimum.toMillis();
+ result = prime * result + (int) maximum.toMillis();
+
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..c4a4a405e5586
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,11 @@
+
+
+
+ binding
+ Energi Data Service Binding
+ This is the binding for Energi Data Service.
+ cloud
+ DK,NO,SE
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..be4a186f2c28d
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+ Price area for spot prices (same as bidding zone).
+ false
+
+
+
+
+
+
+
+ Currency code in which to obtain spot prices.
+ DKK
+
+
+
+
+
+
+
+ Global Location Number of the grid company.
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global Location Number of Energinet.
+ true
+ 5790000432752
+
+
+
+
+
+
+ Add VAT to amount based on regional settings.
+
+
+
+
+
+
+ Add VAT to amount based on regional settings.
+
+
+
+ Comma-separated list of charge type codes.
+ true
+
+
+
+ Comma-separated list of notes.
+ true
+
+
+
+ Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay,
+ StartOfMonth or StartOfYear.
+ false
+
+
+
+
+
+ true
+
+
+
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties
new file mode 100644
index 0000000000000..441f2b974257d
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties
@@ -0,0 +1,108 @@
+# add-on
+
+addon.energidataservice.name = Energi Data Service Binding
+addon.energidataservice.description = This is the binding for Energi Data Service.
+
+# thing types
+
+thing-type.energidataservice.service.label = Energi Data Service
+thing-type.energidataservice.service.description = This thing represents the Energi Data Service API.
+
+# thing types config
+
+thing-type.config.energidataservice.service.currencyCode.label = Currency Code
+thing-type.config.energidataservice.service.currencyCode.description = Currency code in which to obtain spot prices.
+thing-type.config.energidataservice.service.currencyCode.option.DKK = Danish Krone
+thing-type.config.energidataservice.service.currencyCode.option.EUR = Euro
+thing-type.config.energidataservice.service.energinetGLN.label = Energinet GLN
+thing-type.config.energidataservice.service.energinetGLN.description = Global Location Number of Energinet.
+thing-type.config.energidataservice.service.gridCompanyGLN.label = Grid Company GLN
+thing-type.config.energidataservice.service.gridCompanyGLN.description = Global Location Number of the grid company.
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705184 = Cerius
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610099 = Dinel
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790002502699 = El-net Kongerslev
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836239 = Elektrus
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001095277 = Elinord
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001100520 = Elnet Midt
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392551 = FLOW Elnet
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090166 = Hammel Elforsyning Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610839 = Hurup Elværk Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000682102 = Ikast El Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000704842 = Konstant
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090111 = L-Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089030 = N1
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681372 = N1 Randers
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681075 = Netselskabet Elværk
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088231 = NKE-Elnet
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610877 = Nord Energi Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000395620 = Nordvestjysk Elforsyning (NOE Net)
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705689 = Radius
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681327 = RAH
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836727 = Ravdex
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706419 = Tarm Elværk Net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392261 = TREFOR El-net
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706686 = TREFOR El-net Øst
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088217 = Veksel
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610976 = Vores Elnet
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089375 = Zeanet
+thing-type.config.energidataservice.service.priceArea.label = Price Area
+thing-type.config.energidataservice.service.priceArea.description = Price area for spot prices (same as bidding zone).
+thing-type.config.energidataservice.service.priceArea.option.DK1 = West of the Great Belt
+thing-type.config.energidataservice.service.priceArea.option.DK2 = East of the Great Belt
+
+# channel group types
+
+channel-group-type.energidataservice.electricity.label = Electricity
+channel-group-type.energidataservice.electricity.description = Channels related to electricity
+channel-group-type.energidataservice.electricity.channel.currentElectricityTax.label = Current Electricity Tax
+channel-group-type.energidataservice.electricity.channel.currentElectricityTax.description = Electricity Tax in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.currentNetTariff.label = Current Net Tariff
+channel-group-type.energidataservice.electricity.channel.currentNetTariff.description = Net tariff in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.currentSpotPrice.label = Current Spot Price
+channel-group-type.energidataservice.electricity.channel.currentSpotPrice.description = Spot price in DKK or EUR per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.currentSystemTariff.label = Current System Tariff
+channel-group-type.energidataservice.electricity.channel.currentSystemTariff.description = System tariff in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.label = Current Transmission Tariff
+channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.description = Transmission Net Tariff in DKK per kWh for current hour.
+
+# channel types
+
+channel-type.energidataservice.datahubPrice.label = Datahub Price
+channel-type.energidataservice.datahubPrice.description = Datahub price.
+channel-type.energidataservice.hourlyPrices.label = Hourly Prices
+channel-type.energidataservice.hourlyPrices.description = JSON array with hourly prices from 12 hours ago and onward.
+channel-type.energidataservice.spotPrice.label = Spot Price
+channel-type.energidataservice.spotPrice.description = Spot price.
+
+# channel types config
+
+channel-type.config.energidataservice.datahub-price.chargeTypeCodes.label = Charge Type Code Filters
+channel-type.config.energidataservice.datahub-price.chargeTypeCodes.description = Comma-separated list of charge type codes.
+channel-type.config.energidataservice.datahub-price.includeVAT.label = Include VAT
+channel-type.config.energidataservice.datahub-price.includeVAT.description = Add VAT to amount based on regional settings.
+channel-type.config.energidataservice.datahub-price.notes.label = Note Filters
+channel-type.config.energidataservice.datahub-price.notes.description = Comma-separated list of notes.
+channel-type.config.energidataservice.datahub-price.start.label = Query Start Date
+channel-type.config.energidataservice.datahub-price.start.description = Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear.
+channel-type.config.energidataservice.datahub-price.start.option.StartOfDay = Start of day
+channel-type.config.energidataservice.datahub-price.start.option.StartOfMonth = Start of month
+channel-type.config.energidataservice.datahub-price.start.option.StartOfYear = Start of year
+channel-type.config.energidataservice.spot-price.includeVAT.label = Include VAT
+channel-type.config.energidataservice.spot-price.includeVAT.description = Add VAT to amount based on regional settings.
+
+# thing status descriptions
+
+offline.conf-error.no-price-area = Price area must be set
+offline.conf-error.invalid-grid-company-gln = Invalid grid company GLN
+offline.conf-error.invalid-energinet-gln = Invalid Energinet GLN
+
+# actions
+
+action.calculate-cheapest-period.label = calculate cheapest period
+action.calculate-cheapest-period.description = calculate cheapest period for using power according to a supplied timetable (excl. VAT)
+action.calculate-price.label = calculate price
+action.calculate-price.description = calculate price for power consumption in period excl. VAT
+action.get-prices.label = get prices
+action.get-prices.description = get hourly prices excl. VAT
+action.get-prices.priceElements.label = price elements
+action.get-prices.priceElements.description = comma-separated list of price elements to include in sums
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml
new file mode 100644
index 0000000000000..e365653e0022e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Channels related to electricity
+
+
+
+ Spot price in DKK or EUR per kWh for current hour.
+
+
+
+ Net tariff in DKK per kWh for current hour.
+
+
+
+ System tariff in DKK per kWh for current hour.
+
+
+
+ Electricity Tax in DKK per kWh for current hour.
+
+
+
+ Transmission Net Tariff in DKK per kWh for current hour.
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml
new file mode 100644
index 0000000000000..ffab302bd670e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ Number
+
+ Spot price.
+ Price
+
+
+
+
+
+ Number
+
+ Datahub price.
+ Price
+
+
+
+
+
+ String
+
+ JSON array with hourly prices from 12 hours ago and onward.
+ Price
+
+
+
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml
new file mode 100644
index 0000000000000..84e459e170b3e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ This thing represents the Energi Data Service API.
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java
new file mode 100644
index 0000000000000..5deecd5abd94c
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
+
+/**
+ * Tests for {@link CacheManager}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class CacheManagerTest {
+
+ @Test
+ void areSpotPricesFullyCachedToday() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T20:00:00Z");
+ Instant last = Instant.parse("2023-02-07T22:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
+ }
+
+ @Test
+ void areSpotPricesFullyCachedTodayMissingAtStart() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T21:00:00Z");
+ Instant last = Instant.parse("2023-02-07T22:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areSpotPricesFullyCached(), is(false));
+ }
+
+ @Test
+ void areSpotPricesFullyCachedTodayMissingAtEnd() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T20:00:00Z");
+ Instant last = Instant.parse("2023-02-07T21:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areSpotPricesFullyCached(), is(false));
+ }
+
+ @Test
+ void areSpotPricesFullyCachedTodayOtherTimezoneIsIgnored() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T20:00:00Z");
+ Instant last = Instant.parse("2023-02-07T22:00:00Z");
+ Clock clock = Clock.fixed(now, ZoneId.of("Asia/Tokyo"));
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
+ }
+
+ @Test
+ void areSpotPricesFullyCachedTomorrow() {
+ Instant now = Instant.parse("2023-02-07T12:00:00Z");
+ Instant first = Instant.parse("2023-02-07T00:00:00Z");
+ Instant last = Instant.parse("2023-02-08T22:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
+ }
+
+ @Test
+ void areHistoricSpotPricesCached() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T20:00:00Z");
+ Instant last = Instant.parse("2023-02-07T07:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areHistoricSpotPricesCached(), is(true));
+ }
+
+ @Test
+ void areHistoricSpotPricesCachedFirstHourMissing() {
+ Instant now = Instant.parse("2023-02-07T08:38:47Z");
+ Instant first = Instant.parse("2023-02-06T21:00:00Z");
+ Instant last = Instant.parse("2023-02-07T08:00:00Z");
+ Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
+ CacheManager cacheManager = new CacheManager(clock);
+ populateWithSpotPrices(cacheManager, first, last);
+ assertThat(cacheManager.areHistoricSpotPricesCached(), is(false));
+ }
+
+ private void populateWithSpotPrices(CacheManager cacheManager, Instant first, Instant last) {
+ int size = (int) Duration.between(first, last).getSeconds() / 60 / 60 + 1;
+ ElspotpriceRecord[] records = new ElspotpriceRecord[size];
+ int i = 0;
+ for (Instant hourStart = first; !hourStart.isAfter(last); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) {
+ records[i++] = new ElspotpriceRecord(hourStart, BigDecimal.ONE, BigDecimal.ZERO);
+ }
+ cacheManager.putSpotPrices(records, EnergiDataServiceBindingConstants.CURRENCY_DKK);
+ }
+}
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
new file mode 100644
index 0000000000000..689966a500bd4
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
+import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Tests for {@link PriceListParser}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class PriceListParserTest {
+
+ private Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer())
+ .create();
+
+ private T getObjectFromJson(String filename, Class clazz) throws IOException {
+ try (InputStream inputStream = PriceListParserTest.class.getResourceAsStream(filename)) {
+ if (inputStream == null) {
+ throw new IOException("Input stream is null");
+ }
+ byte[] bytes = inputStream.readAllBytes();
+ if (bytes == null) {
+ throw new IOException("Resulting byte-array empty");
+ }
+ String json = new String(bytes, StandardCharsets.UTF_8);
+ return Objects.requireNonNull(gson.fromJson(json, clazz));
+ }
+ }
+
+ @Test
+ void toHourlyNoChanges() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2023-01-23T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(48));
+ assertThat(tariffMap.get(Instant.parse("2023-01-23T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-23T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-24T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-24T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
+ }
+
+ @Test
+ void toHourlyNewTariffTomorrowWhenSummertime() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2023-03-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(48));
+ assertThat(tariffMap.get(Instant.parse("2023-03-31T14:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-03-31T15:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
+ assertThat(tariffMap.get(Instant.parse("2023-04-01T14:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-04-01T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ }
+
+ @Test
+ 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");
+
+ assertThat(tariffMap.size(), is(48));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717"))));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ }
+
+ @Test
+ 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");
+
+ assertThat(tariffMap.size(), is(48));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717"))));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.0"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.0"))));
+ }
+
+ @Test
+ void toHourlyTariffAndDiscountIsSum() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2022-11-30T15:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(45));
+ assertThat(tariffMap.get(Instant.parse("2022-11-30T15:00:00Z")), is(equalTo(new BigDecimal("0.387517"))));
+ assertThat(tariffMap.get(Instant.parse("2022-11-30T16:00:00Z")), is(equalTo(new BigDecimal("0.973404"))));
+ }
+
+ @Test
+ void toHourlyTariffAndDiscountIsFree() 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());
+
+ assertThat(tariffMap.size(), is(48));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T16:00:00Z")), is(equalTo(new BigDecimal("0.000000"))));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.000000"))));
+ assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ }
+
+ @Test
+ void toHourlyFixedTariff() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2022-12-31T23:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistNordEnergi.json",
+ DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(25)); // No records in dataset before 2023-01-01
+ for (Instant i = Instant.parse("2022-12-31T23:00:00Z"); i
+ .isBefore(Instant.parse("2023-01-02T00:00:00Z")); i = i.plus(1, ChronoUnit.HOURS)) {
+ assertThat(tariffMap.get(i), is(equalTo(new BigDecimal("0.245"))));
+ }
+ }
+
+ @Test
+ void toHourlyDailyTariffs() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2023-01-28T04:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistTrefor.json",
+ DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(56));
+ assertThat(tariffMap.get(Instant.parse("2023-01-28T04:00:00Z")), is(equalTo(new BigDecimal("0.2581"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-28T05:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-28T16:00:00Z")), is(equalTo(new BigDecimal("2.3227"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-28T20:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-28T23:00:00Z")), is(equalTo(new BigDecimal("0.2581"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-29T05:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-29T16:00:00Z")), is(equalTo(new BigDecimal("2.3227"))));
+ assertThat(tariffMap.get(Instant.parse("2023-01-29T20:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
+ }
+
+ @Test
+ void toHourlySystemTariff() throws IOException {
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(Instant.parse("2023-06-30T21:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistElectricityTax.json",
+ DatahubPricelistRecords.class);
+ Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+ assertThat(tariffMap.size(), is(39));
+ assertThat(tariffMap.get(Instant.parse("2023-06-30T21:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
+ assertThat(tariffMap.get(Instant.parse("2023-06-30T22:00:00Z")), is(equalTo(new BigDecimal("0.697"))));
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java
new file mode 100644
index 0000000000000..8c5f73a25e72f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java
@@ -0,0 +1,403 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.action;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants;
+import org.openhab.binding.energidataservice.internal.PriceListParser;
+import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
+import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
+import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
+import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+
+/**
+ * Tests for {@link EnergiDataServiceActions}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class EnergiDataServiceActionsTest {
+
+ private @NonNullByDefault({}) @Mock EnergiDataServiceHandler handler;
+ private EnergiDataServiceActions actions = new EnergiDataServiceActions();
+
+ private Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer())
+ .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()).create();
+
+ private record SpotPrice(Instant hourStart, BigDecimal spotPrice) {
+ }
+
+ private T getObjectFromJson(String filename, Class clazz) throws IOException {
+ try (InputStream inputStream = EnergiDataServiceActionsTest.class.getResourceAsStream(filename)) {
+ if (inputStream == null) {
+ throw new IOException("Input stream is null");
+ }
+ byte[] bytes = inputStream.readAllBytes();
+ if (bytes == null) {
+ throw new IOException("Resulting byte-array empty");
+ }
+ String json = new String(bytes, StandardCharsets.UTF_8);
+ return Objects.requireNonNull(gson.fromJson(json, clazz));
+ }
+ }
+
+ @BeforeEach
+ private void setUp() {
+ final Logger logger = (Logger) LoggerFactory.getLogger(EnergiDataServiceActions.class);
+ logger.setLevel(Level.OFF);
+
+ actions = new EnergiDataServiceActions();
+ }
+
+ @Test
+ void getPricesSpotPrice() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("SpotPrice");
+ assertThat(actual.size(), is(35));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.992840027"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.267680054"))));
+ }
+
+ @Test
+ void getPricesNetTariff() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("NetTariff");
+ assertThat(actual.size(), is(48));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
+ }
+
+ @Test
+ void getPricesSystemTariff() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("SystemTariff");
+ assertThat(actual.size(), is(48));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
+ }
+
+ @Test
+ void getPricesElectricityTax() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("ElectricityTax");
+ assertThat(actual.size(), is(48));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
+ }
+
+ @Test
+ void getPricesTransmissionNetTariff() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("TransmissionNetTariff");
+ assertThat(actual.size(), is(48));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
+ }
+
+ @Test
+ void getPricesSpotPriceNetTariff() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("SpotPrice,NetTariff");
+ assertThat(actual.size(), is(35));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.425065027"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.323870054"))));
+ }
+
+ @Test
+ void getPricesSpotPriceNetTariffElectricityTax() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("SpotPrice,NetTariff,ElectricityTax");
+ assertThat(actual.size(), is(35));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.433065027"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.331870054"))));
+ }
+
+ @Test
+ void getPricesTotal() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices();
+ assertThat(actual.size(), is(35));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
+ }
+
+ @Test
+ void getPricesTotalAllElements() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions
+ .getPrices("spotprice,nettariff,systemtariff,electricitytax,transmissionnettariff");
+ assertThat(actual.size(), is(35));
+ assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T15:00:00Z")), is(equalTo(new BigDecimal("1.708765039"))));
+ assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
+ }
+
+ @Test
+ void getPricesInvalidPriceElement() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.getPrices("spotprice,nettarif");
+ assertThat(actual.size(), is(0));
+ }
+
+ @Test
+ void getPricesMixedCurrencies() throws IOException {
+ mockCommonDatasets(actions);
+ when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_EUR);
+
+ Map actual = actions.getPrices("spotprice,nettariff");
+ assertThat(actual.size(), is(0));
+ }
+
+ /**
+ * Calculate price in period 15:30-16:30 (UTC) with consumption 150 W and the following total prices:
+ * 15:00:00: 1.708765039
+ * 16:00:00: 2.443870054
+ *
+ * Result = (1.708765039 / 2) + (2.443870054 / 2) * 0.150
+ *
+ * @throws IOException
+ */
+ @Test
+ void calculatePriceSimple() throws IOException {
+ mockCommonDatasets(actions);
+
+ BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:30:00Z"),
+ Instant.parse("2023-02-04T16:30:00Z"), new QuantityType<>(150, Units.WATT));
+ assertThat(actual, is(equalTo(new BigDecimal("0.311447631975000000")))); // 0.3114476319750
+ }
+
+ /**
+ * Calculate price in period 15:00-17:00 (UTC) with consumption 1000 W and the following total prices:
+ * 15:00:00: 1.708765039
+ * 16:00:00: 2.443870054
+ *
+ * Result = 1.708765039 + 2.443870054
+ *
+ * @throws IOException
+ */
+ @Test
+ void calculatePriceFullHours() throws IOException {
+ mockCommonDatasets(actions);
+
+ BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:00:00Z"),
+ Instant.parse("2023-02-04T17:00:00Z"), new QuantityType<>(1, Units.KILOVAR));
+ assertThat(actual, is(equalTo(new BigDecimal("4.152635093000000000")))); // 4.152635093
+ }
+
+ @Test
+ void calculatePriceOutOfRangeStart() throws IOException {
+ mockCommonDatasets(actions);
+
+ BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-03T23:59:00Z"),
+ Instant.parse("2023-02-04T12:30:00Z"), new QuantityType<>(1000, Units.WATT));
+ assertThat(actual, is(equalTo(BigDecimal.ZERO)));
+ }
+
+ @Test
+ void calculatePriceOutOfRangeEnd() throws IOException {
+ mockCommonDatasets(actions);
+
+ BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-05T22:00:00Z"),
+ Instant.parse("2023-02-05T23:01:00Z"), new QuantityType<>(1000, Units.WATT));
+ assertThat(actual, is(equalTo(BigDecimal.ZERO)));
+ }
+
+ /**
+ * Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
+ *
+ * @throws IOException
+ */
+ @Test
+ void calculateCheapestPeriodWithPowerDishwasher() throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230205.json");
+
+ List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
+ Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41),
+ Duration.ofMinutes(104));
+ List> consumptions = List.of(QuantityType.valueOf(162.162162, Units.WATT),
+ QuantityType.valueOf(750, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
+ QuantityType.valueOf(3000, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
+ QuantityType.valueOf(166.666666, Units.WATT), QuantityType.valueOf(146.341463, Units.WATT),
+ QuantityType.valueOf(0, Units.WATT));
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
+ Instant.parse("2023-02-06T06:00:00Z"), durations, consumptions);
+ assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
+ assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
+ assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
+ assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
+ }
+
+ @Test
+ void calculateCheapestPeriodWithPowerOutOfRange() throws IOException {
+ mockCommonDatasets(actions);
+
+ List durations = List.of(Duration.ofMinutes(61));
+ List> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT));
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
+ Instant.parse("2023-02-06T00:01:00Z"), durations, consumptions);
+ assertThat(actual.size(), is(equalTo(0)));
+ }
+
+ /**
+ * Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
+ *
+ * @throws IOException
+ */
+ @Test
+ void calculateCheapestPeriodWithEnergyDishwasher() throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230205.json");
+
+ List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
+ Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41));
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
+ Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236), durations,
+ QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
+ assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
+ assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
+ assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
+ assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
+ }
+
+ @Test
+ void calculateCheapestPeriodWithEnergyTotalDurationIsExactSum() throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230205.json");
+
+ List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
+ Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(120), durations,
+ QuantityType.valueOf(100, Units.WATT_HOUR));
+ assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("0.293540001200000000"))));
+ assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
+ }
+
+ @Test
+ void calculateCheapestPeriodWithEnergyTotalDurationInvalid() throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230205.json");
+
+ List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
+ Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(119), durations,
+ QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
+ assertThat(actual.size(), is(equalTo(0)));
+ }
+
+ /**
+ * Like {@link #calculateCheapestPeriodWithEnergyDishwasher} but with unknown consumption/timetable map.
+ *
+ * @throws IOException
+ */
+ @Test
+ void calculateCheapestPeriodAssumingLinearUnknownConsumption() throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230205.json");
+
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
+ Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236));
+ assertThat(actual.get("LowestPrice"), is(nullValue()));
+ assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
+ assertThat(actual.get("HighestPrice"), is(nullValue()));
+ assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
+ }
+
+ @Test
+ void calculateCheapestPeriodForLinearPowerUsage() throws IOException {
+ mockCommonDatasets(actions);
+
+ Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
+ Instant.parse("2023-02-05T23:00:00Z"), Duration.ofMinutes(61), QuantityType.valueOf(1000, Units.WATT));
+ assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.323990859575000000"))));
+ assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T12:00:00Z"))));
+ assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("2.589061780353348000"))));
+ assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-04T17:00:00Z"))));
+ }
+
+ private void mockCommonDatasets(EnergiDataServiceActions actions) throws IOException {
+ mockCommonDatasets(actions, "SpotPrices20230204.json");
+ }
+
+ private void mockCommonDatasets(EnergiDataServiceActions actions, String spotPricesFilename) throws IOException {
+ SpotPrice[] spotPriceRecords = getObjectFromJson(spotPricesFilename, SpotPrice[].class);
+ Map spotPrices = Arrays.stream(spotPriceRecords)
+ .collect(Collectors.toMap(SpotPrice::hourStart, SpotPrice::spotPrice));
+
+ PriceListParser priceListParser = new PriceListParser(
+ Clock.fixed(spotPriceRecords[0].hourStart, EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
+ DatahubPricelistRecords datahubRecords = getObjectFromJson("NetTariffs.json", DatahubPricelistRecords.class);
+ Map netTariffs = priceListParser
+ .toHourly(Arrays.stream(datahubRecords.records()).toList());
+ datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class);
+ Map systemTariffs = priceListParser
+ .toHourly(Arrays.stream(datahubRecords.records()).toList());
+ datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class);
+ Map electricityTaxes = priceListParser
+ .toHourly(Arrays.stream(datahubRecords.records()).toList());
+ datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class);
+ Map transmissionNetTariffs = priceListParser
+ .toHourly(Arrays.stream(datahubRecords.records()).toList());
+
+ when(handler.getSpotPrices()).thenReturn(spotPrices);
+ when(handler.getNetTariffs()).thenReturn(netTariffs);
+ when(handler.getSystemTariffs()).thenReturn(systemTariffs);
+ when(handler.getElectricityTaxes()).thenReturn(electricityTaxes);
+ when(handler.getTransmissionNetTariffs()).thenReturn(transmissionNetTariffs);
+ when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_DKK);
+ actions.setThingHandler(handler);
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java
new file mode 100644
index 0000000000000..9334b3d903c47
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.time.Duration;
+import java.time.LocalDate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for {@link DateQueryParameter}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class DateQueryParameterTest {
+
+ @Test
+ void dateQueryParameterTypeWithNegativeOffset() {
+ DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(-12));
+ assertThat(parameter.toString(), is(equalTo("utcnow-PT12H")));
+ }
+
+ @Test
+ void dateQueryParameterTypeWithPositiveOffset() {
+ DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(12));
+ assertThat(parameter.toString(), is(equalTo("utcnow+PT12H")));
+ }
+
+ @Test
+ void dateQueryParameterTypeWithZeroOffset() {
+ DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ZERO);
+ assertThat(parameter.toString(), is(equalTo("utcnow")));
+ }
+
+ @Test
+ void dateQueryParameterTypeWithoutOffset() {
+ DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.NOW);
+ assertThat(parameter.toString(), is(equalTo("now")));
+ }
+
+ @Test
+ void localDate() {
+ DateQueryParameter parameter = DateQueryParameter.of(LocalDate.of(2023, 2, 28));
+ assertThat(parameter.toString(), is(equalTo("2023-02-28")));
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java
new file mode 100644
index 0000000000000..15e135e2c38a6
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.api;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for {@link GlobalLocationNumber}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class GlobalLocationNumberTest {
+
+ @Test
+ void isValid() {
+ assertThat(GlobalLocationNumber.of("5790000682102").isValid(), is(true));
+ }
+
+ @Test
+ void isInvalid() {
+ assertThat(GlobalLocationNumber.of("5790000682103").isValid(), is(false));
+ }
+
+ @Test
+ void emptyIsInvalid() {
+ assertThat(GlobalLocationNumber.EMPTY.isValid(), is(false));
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java
new file mode 100644
index 0000000000000..90a6c43177a1f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.time.Duration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * Tests for {@link ExponentialBackoff}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ExponentialBackoffTest {
+
+ @Test
+ void exponential() {
+ RetryStrategy retryPolicy = new ExponentialBackoff().withMinimum(Duration.ofSeconds(2)).withJitter(0.0);
+ for (long i = 2; i <= 256; i *= 2) {
+ assertThat(retryPolicy.getDuration().toSeconds(), is(i));
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java
new file mode 100644
index 0000000000000..22f4b21a75364
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * Tests for {@link FixedTime}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class FixedTimeTest {
+
+ @Test
+ void beforeNoon() {
+ RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0),
+ Clock.fixed(Instant.parse("2023-01-24T10:00:00Z"), ZoneId.of("UTC")));
+ assertThat(retryPolicy.getDuration(), is(Duration.ofHours(2)));
+ }
+
+ @Test
+ void atNoon() {
+ RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0),
+ Clock.fixed(Instant.parse("2023-01-24T12:00:00Z"), ZoneId.of("UTC")));
+ assertThat(retryPolicy.getDuration(), is(Duration.ZERO));
+ }
+
+ @Test
+ void afterNoon() {
+ RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0),
+ Clock.fixed(Instant.parse("2023-01-24T13:00:00Z"), ZoneId.of("UTC")));
+ assertThat(retryPolicy.getDuration(), is(Duration.ofHours(23)));
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java
new file mode 100644
index 0000000000000..4d8b6abe2a8a9
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.energidataservice.internal.retry.strategy;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.time.Duration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+
+/**
+ * Tests for {@link Linear}.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class LinearTest {
+
+ @Test
+ void linear() {
+ RetryStrategy retryPolicy = new Linear().withMinimum(Duration.ofMinutes(1)).withJitter(0.0);
+ for (int i = 0; i <= 10; i++) {
+ assertThat(retryPolicy.getDuration(), is(Duration.ofMinutes(1)));
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json
new file mode 100644
index 0000000000000..fce1f184d3c36
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json
@@ -0,0 +1,83 @@
+{
+ "total": 2,
+ "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}",
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ChargeOwner": "Energinet Systemansvar A/S (SYO)",
+ "GLN_Number": "5790000432752",
+ "ChargeType": "D03",
+ "ChargeTypeCode": "EA-001",
+ "Note": "Elafgift",
+ "Description": "Elafgiften",
+ "ValidFrom": "2023-07-01T00:00:00",
+ "ValidTo": null,
+ "VATClass": "D02",
+ "Price1": 0.697,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null,
+ "TransparentInvoicing": 1,
+ "TaxIndicator": 1,
+ "ResolutionDuration": "P1D"
+ },
+ {
+ "ChargeOwner": "Energinet Systemansvar A/S (SYO)",
+ "GLN_Number": "5790000432752",
+ "ChargeType": "D03",
+ "ChargeTypeCode": "EA-001",
+ "Note": "Elafgift",
+ "Description": "Elafgiften",
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": "2023-07-01T00:00:00",
+ "VATClass": "D02",
+ "Price1": 0.008,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null,
+ "TransparentInvoicing": 1,
+ "TaxIndicator": 1,
+ "ResolutionDuration": "P1D"
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json
new file mode 100644
index 0000000000000..8d265147a1fbc
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json
@@ -0,0 +1,588 @@
+{
+ "total": 20,
+ "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}",
+ "limit": 100,
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-04-01T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "CD",
+ "Price1": 0.432225,
+ "Price2": 0.432225,
+ "Price3": 0.432225,
+ "Price4": 0.432225,
+ "Price5": 0.432225,
+ "Price6": 0.432225,
+ "Price7": 0.432225,
+ "Price8": 0.432225,
+ "Price9": 0.432225,
+ "Price10": 0.432225,
+ "Price11": 0.432225,
+ "Price12": 0.432225,
+ "Price13": 0.432225,
+ "Price14": 0.432225,
+ "Price15": 0.432225,
+ "Price16": 0.432225,
+ "Price17": 0.432225,
+ "Price18": 0.432225,
+ "Price19": 0.432225,
+ "Price20": 0.432225,
+ "Price21": 0.432225,
+ "Price22": 0.432225,
+ "Price23": 0.432225,
+ "Price24": 0.432225
+ },
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": "2023-04-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.432225,
+ "Price2": 0.432225,
+ "Price3": 0.432225,
+ "Price4": 0.432225,
+ "Price5": 0.432225,
+ "Price6": 0.432225,
+ "Price7": 0.432225,
+ "Price8": 0.432225,
+ "Price9": 0.432225,
+ "Price10": 0.432225,
+ "Price11": 0.432225,
+ "Price12": 0.432225,
+ "Price13": 0.432225,
+ "Price14": 0.432225,
+ "Price15": 0.432225,
+ "Price16": 0.432225,
+ "Price17": 0.432225,
+ "Price18": 1.05619,
+ "Price19": 1.05619,
+ "Price20": 1.05619,
+ "Price21": 0.432225,
+ "Price22": 0.432225,
+ "Price23": 0.432225,
+ "Price24": 0.432225
+ },
+ {
+ "ValidFrom": "2022-11-01T00:00:00",
+ "ValidTo": "2023-01-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.407717,
+ "Price2": 0.407717,
+ "Price3": 0.407717,
+ "Price4": 0.407717,
+ "Price5": 0.407717,
+ "Price6": 0.407717,
+ "Price7": 0.407717,
+ "Price8": 0.407717,
+ "Price9": 0.407717,
+ "Price10": 0.407717,
+ "Price11": 0.407717,
+ "Price12": 0.407717,
+ "Price13": 0.407717,
+ "Price14": 0.407717,
+ "Price15": 0.407717,
+ "Price16": 0.407717,
+ "Price17": 0.407717,
+ "Price18": 1.015888,
+ "Price19": 1.015888,
+ "Price20": 1.015888,
+ "Price21": 0.407717,
+ "Price22": 0.407717,
+ "Price23": 0.407717,
+ "Price24": 0.407717
+ },
+ {
+ "ValidFrom": "2022-10-01T00:00:00",
+ "ValidTo": "2022-11-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.31535,
+ "Price2": 0.31535,
+ "Price3": 0.31535,
+ "Price4": 0.31535,
+ "Price5": 0.31535,
+ "Price6": 0.31535,
+ "Price7": 0.31535,
+ "Price8": 0.31535,
+ "Price9": 0.31535,
+ "Price10": 0.31535,
+ "Price11": 0.31535,
+ "Price12": 0.31535,
+ "Price13": 0.31535,
+ "Price14": 0.31535,
+ "Price15": 0.31535,
+ "Price16": 0.31535,
+ "Price17": 0.31535,
+ "Price18": 0.821619,
+ "Price19": 0.821619,
+ "Price20": 0.821619,
+ "Price21": 0.31535,
+ "Price22": 0.31535,
+ "Price23": 0.31535,
+ "Price24": 0.31535
+ },
+ {
+ "ValidFrom": "2022-08-01T00:00:00",
+ "ValidTo": "2022-10-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.31535,
+ "Price2": 0.31535,
+ "Price3": 0.31535,
+ "Price4": 0.31535,
+ "Price5": 0.31535,
+ "Price6": 0.31535,
+ "Price7": 0.31535,
+ "Price8": 0.31535,
+ "Price9": 0.31535,
+ "Price10": 0.31535,
+ "Price11": 0.31535,
+ "Price12": 0.31535,
+ "Price13": 0.31535,
+ "Price14": 0.31535,
+ "Price15": 0.31535,
+ "Price16": 0.31535,
+ "Price17": 0.31535,
+ "Price18": 0.31535,
+ "Price19": 0.31535,
+ "Price20": 0.31535,
+ "Price21": 0.31535,
+ "Price22": 0.31535,
+ "Price23": 0.31535,
+ "Price24": 0.31535
+ },
+ {
+ "ValidFrom": "2022-04-01T00:00:00",
+ "ValidTo": "2022-08-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.227969,
+ "Price2": 0.227969,
+ "Price3": 0.227969,
+ "Price4": 0.227969,
+ "Price5": 0.227969,
+ "Price6": 0.227969,
+ "Price7": 0.227969,
+ "Price8": 0.227969,
+ "Price9": 0.227969,
+ "Price10": 0.227969,
+ "Price11": 0.227969,
+ "Price12": 0.227969,
+ "Price13": 0.227969,
+ "Price14": 0.227969,
+ "Price15": 0.227969,
+ "Price16": 0.227969,
+ "Price17": 0.227969,
+ "Price18": 0.227969,
+ "Price19": 0.227969,
+ "Price20": 0.227969,
+ "Price21": 0.227969,
+ "Price22": 0.227969,
+ "Price23": 0.227969,
+ "Price24": 0.227969
+ },
+ {
+ "ValidFrom": "2022-01-01T00:00:00",
+ "ValidTo": "2022-04-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.183226,
+ "Price2": 0.183226,
+ "Price3": 0.183226,
+ "Price4": 0.183226,
+ "Price5": 0.183226,
+ "Price6": 0.183226,
+ "Price7": 0.183226,
+ "Price8": 0.183226,
+ "Price9": 0.183226,
+ "Price10": 0.183226,
+ "Price11": 0.183226,
+ "Price12": 0.183226,
+ "Price13": 0.183226,
+ "Price14": 0.183226,
+ "Price15": 0.183226,
+ "Price16": 0.183226,
+ "Price17": 0.183226,
+ "Price18": 0.543732,
+ "Price19": 0.543732,
+ "Price20": 0.543732,
+ "Price21": 0.183226,
+ "Price22": 0.183226,
+ "Price23": 0.183226,
+ "Price24": 0.183226
+ },
+ {
+ "ValidFrom": "2021-10-01T00:00:00",
+ "ValidTo": "2022-01-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.1717,
+ "Price2": 0.1717,
+ "Price3": 0.1717,
+ "Price4": 0.1717,
+ "Price5": 0.1717,
+ "Price6": 0.1717,
+ "Price7": 0.1717,
+ "Price8": 0.1717,
+ "Price9": 0.1717,
+ "Price10": 0.1717,
+ "Price11": 0.1717,
+ "Price12": 0.1717,
+ "Price13": 0.1717,
+ "Price14": 0.1717,
+ "Price15": 0.1717,
+ "Price16": 0.1717,
+ "Price17": 0.1717,
+ "Price18": 0.5448,
+ "Price19": 0.5448,
+ "Price20": 0.5448,
+ "Price21": 0.1717,
+ "Price22": 0.1717,
+ "Price23": 0.1717,
+ "Price24": 0.1717
+ },
+ {
+ "ValidFrom": "2021-04-01T00:00:00",
+ "ValidTo": "2021-10-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.1717,
+ "Price2": 0.1717,
+ "Price3": 0.1717,
+ "Price4": 0.1717,
+ "Price5": 0.1717,
+ "Price6": 0.1717,
+ "Price7": 0.1717,
+ "Price8": 0.1717,
+ "Price9": 0.1717,
+ "Price10": 0.1717,
+ "Price11": 0.1717,
+ "Price12": 0.1717,
+ "Price13": 0.1717,
+ "Price14": 0.1717,
+ "Price15": 0.1717,
+ "Price16": 0.1717,
+ "Price17": 0.1717,
+ "Price18": 0.1717,
+ "Price19": 0.1717,
+ "Price20": 0.1717,
+ "Price21": 0.1717,
+ "Price22": 0.1717,
+ "Price23": 0.1717,
+ "Price24": 0.1717
+ },
+ {
+ "ValidFrom": "2021-01-01T00:00:00",
+ "ValidTo": "2021-04-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.1717,
+ "Price2": 0.1717,
+ "Price3": 0.1717,
+ "Price4": 0.1717,
+ "Price5": 0.1717,
+ "Price6": 0.1717,
+ "Price7": 0.1717,
+ "Price8": 0.1717,
+ "Price9": 0.1717,
+ "Price10": 0.1717,
+ "Price11": 0.1717,
+ "Price12": 0.1717,
+ "Price13": 0.1717,
+ "Price14": 0.1717,
+ "Price15": 0.1717,
+ "Price16": 0.1717,
+ "Price17": 0.1717,
+ "Price18": 0.5448,
+ "Price19": 0.5448,
+ "Price20": 0.5448,
+ "Price21": 0.1717,
+ "Price22": 0.1717,
+ "Price23": 0.1717,
+ "Price24": 0.1717
+ },
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "CD R",
+ "Price1": 0.0,
+ "Price2": 0.0,
+ "Price3": 0.0,
+ "Price4": 0.0,
+ "Price5": 0.0,
+ "Price6": 0.0,
+ "Price7": 0.0,
+ "Price8": 0.0,
+ "Price9": 0.0,
+ "Price10": 0.0,
+ "Price11": 0.0,
+ "Price12": 0.0,
+ "Price13": 0.0,
+ "Price14": 0.0,
+ "Price15": 0.0,
+ "Price16": 0.0,
+ "Price17": 0.0,
+ "Price18": 0.0,
+ "Price19": 0.0,
+ "Price20": 0.0,
+ "Price21": 0.0,
+ "Price22": 0.0,
+ "Price23": 0.0,
+ "Price24": 0.0
+ },
+ {
+ "ValidFrom": "2022-12-01T00:00:00",
+ "ValidTo": "2023-01-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.407717,
+ "Price2": -0.407717,
+ "Price3": -0.407717,
+ "Price4": -0.407717,
+ "Price5": -0.407717,
+ "Price6": -0.407717,
+ "Price7": -0.407717,
+ "Price8": -0.407717,
+ "Price9": -0.407717,
+ "Price10": -0.407717,
+ "Price11": -0.407717,
+ "Price12": -0.407717,
+ "Price13": -0.407717,
+ "Price14": -0.407717,
+ "Price15": -0.407717,
+ "Price16": -0.407717,
+ "Price17": -0.407717,
+ "Price18": -1.015888,
+ "Price19": -1.015888,
+ "Price20": -1.015888,
+ "Price21": -0.407717,
+ "Price22": -0.407717,
+ "Price23": -0.407717,
+ "Price24": -0.407717
+ },
+ {
+ "ValidFrom": "2022-10-01T00:00:00",
+ "ValidTo": "2022-12-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0202,
+ "Price2": -0.0202,
+ "Price3": -0.0202,
+ "Price4": -0.0202,
+ "Price5": -0.0202,
+ "Price6": -0.0202,
+ "Price7": -0.0202,
+ "Price8": -0.0202,
+ "Price9": -0.0202,
+ "Price10": -0.0202,
+ "Price11": -0.0202,
+ "Price12": -0.0202,
+ "Price13": -0.0202,
+ "Price14": -0.0202,
+ "Price15": -0.0202,
+ "Price16": -0.0202,
+ "Price17": -0.0202,
+ "Price18": -0.042484,
+ "Price19": -0.042484,
+ "Price20": -0.042484,
+ "Price21": -0.0202,
+ "Price22": -0.0202,
+ "Price23": -0.0202,
+ "Price24": -0.0202
+ },
+ {
+ "ValidFrom": "2022-08-01T00:00:00",
+ "ValidTo": "2022-10-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0202,
+ "Price2": -0.0202,
+ "Price3": -0.0202,
+ "Price4": -0.0202,
+ "Price5": -0.0202,
+ "Price6": -0.0202,
+ "Price7": -0.0202,
+ "Price8": -0.0202,
+ "Price9": -0.0202,
+ "Price10": -0.0202,
+ "Price11": -0.0202,
+ "Price12": -0.0202,
+ "Price13": -0.0202,
+ "Price14": -0.0202,
+ "Price15": -0.0202,
+ "Price16": -0.0202,
+ "Price17": -0.0202,
+ "Price18": -0.0202,
+ "Price19": -0.0202,
+ "Price20": -0.0202,
+ "Price21": -0.0202,
+ "Price22": -0.0202,
+ "Price23": -0.0202,
+ "Price24": -0.0202
+ },
+ {
+ "ValidFrom": "2022-04-01T00:00:00",
+ "ValidTo": "2022-08-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0202,
+ "Price2": -0.0202,
+ "Price3": -0.0202,
+ "Price4": -0.0202,
+ "Price5": -0.0202,
+ "Price6": -0.0202,
+ "Price7": -0.0202,
+ "Price8": -0.0202,
+ "Price9": -0.0202,
+ "Price10": -0.0202,
+ "Price11": -0.0202,
+ "Price12": -0.0202,
+ "Price13": -0.0202,
+ "Price14": -0.0202,
+ "Price15": -0.0202,
+ "Price16": -0.0202,
+ "Price17": -0.0202,
+ "Price18": -0.0202,
+ "Price19": -0.0202,
+ "Price20": -0.0202,
+ "Price21": -0.0202,
+ "Price22": -0.0202,
+ "Price23": -0.0202,
+ "Price24": -0.0202
+ },
+ {
+ "ValidFrom": "2022-01-01T00:00:00",
+ "ValidTo": "2022-04-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0202,
+ "Price2": -0.0202,
+ "Price3": -0.0202,
+ "Price4": -0.0202,
+ "Price5": -0.0202,
+ "Price6": -0.0202,
+ "Price7": -0.0202,
+ "Price8": -0.0202,
+ "Price9": -0.0202,
+ "Price10": -0.0202,
+ "Price11": -0.0202,
+ "Price12": -0.0202,
+ "Price13": -0.0202,
+ "Price14": -0.0202,
+ "Price15": -0.0202,
+ "Price16": -0.0202,
+ "Price17": -0.0202,
+ "Price18": -0.042484,
+ "Price19": -0.042484,
+ "Price20": -0.042484,
+ "Price21": -0.0202,
+ "Price22": -0.0202,
+ "Price23": -0.0202,
+ "Price24": -0.0202
+ },
+ {
+ "ValidFrom": "2021-10-01T00:00:00",
+ "ValidTo": "2022-01-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0224,
+ "Price2": -0.0224,
+ "Price3": -0.0224,
+ "Price4": -0.0224,
+ "Price5": -0.0224,
+ "Price6": -0.0224,
+ "Price7": -0.0224,
+ "Price8": -0.0224,
+ "Price9": -0.0224,
+ "Price10": -0.0224,
+ "Price11": -0.0224,
+ "Price12": -0.0224,
+ "Price13": -0.0224,
+ "Price14": -0.0224,
+ "Price15": -0.0224,
+ "Price16": -0.0224,
+ "Price17": -0.0224,
+ "Price18": -0.0471,
+ "Price19": -0.0471,
+ "Price20": -0.0471,
+ "Price21": -0.0224,
+ "Price22": -0.0224,
+ "Price23": -0.0224,
+ "Price24": -0.0224
+ },
+ {
+ "ValidFrom": "2021-04-01T00:00:00",
+ "ValidTo": "2021-10-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0224,
+ "Price2": -0.0224,
+ "Price3": -0.0224,
+ "Price4": -0.0224,
+ "Price5": -0.0224,
+ "Price6": -0.0224,
+ "Price7": -0.0224,
+ "Price8": -0.0224,
+ "Price9": -0.0224,
+ "Price10": -0.0224,
+ "Price11": -0.0224,
+ "Price12": -0.0224,
+ "Price13": -0.0224,
+ "Price14": -0.0224,
+ "Price15": -0.0224,
+ "Price16": -0.0224,
+ "Price17": -0.0224,
+ "Price18": -0.0224,
+ "Price19": -0.0224,
+ "Price20": -0.0224,
+ "Price21": -0.0224,
+ "Price22": -0.0224,
+ "Price23": -0.0224,
+ "Price24": -0.0224
+ },
+ {
+ "ValidFrom": "2021-03-01T00:00:00",
+ "ValidTo": "2021-04-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0224,
+ "Price2": -0.0224,
+ "Price3": -0.0224,
+ "Price4": -0.0224,
+ "Price5": -0.0224,
+ "Price6": -0.0224,
+ "Price7": -0.0224,
+ "Price8": -0.0224,
+ "Price9": -0.0224,
+ "Price10": -0.0224,
+ "Price11": -0.0224,
+ "Price12": -0.0224,
+ "Price13": -0.0224,
+ "Price14": -0.0224,
+ "Price15": -0.0224,
+ "Price16": -0.0224,
+ "Price17": -0.0224,
+ "Price18": -0.0471,
+ "Price19": -0.0471,
+ "Price20": -0.0471,
+ "Price21": -0.0224,
+ "Price22": -0.0224,
+ "Price23": -0.0224,
+ "Price24": -0.0224
+ },
+ {
+ "ValidFrom": "2021-01-01T00:00:00",
+ "ValidTo": "2021-03-01T00:00:00",
+ "ChargeTypeCode": "CD R",
+ "Price1": -0.0224,
+ "Price2": -0.0224,
+ "Price3": -0.0224,
+ "Price4": -0.0224,
+ "Price5": -0.0224,
+ "Price6": -0.0224,
+ "Price7": -0.0224,
+ "Price8": -0.0224,
+ "Price9": -0.0224,
+ "Price10": -0.0224,
+ "Price11": -0.0224,
+ "Price12": -0.0224,
+ "Price13": -0.0224,
+ "Price14": -0.0224,
+ "Price15": -0.0224,
+ "Price16": -0.0224,
+ "Price17": -0.0471,
+ "Price18": -0.0471,
+ "Price19": -0.0471,
+ "Price20": -0.0471,
+ "Price21": -0.0224,
+ "Price22": -0.0224,
+ "Price23": -0.0224,
+ "Price24": -0.0224
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json
new file mode 100644
index 0000000000000..7641d136c6d4d
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json
@@ -0,0 +1,37 @@
+{
+ "total": 1,
+ "filters": "{\"Note\":[\"Nettarif C\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000610877\"]}",
+ "limit": 100,
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "TA031U200",
+ "Price1": 0.245,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json
new file mode 100644
index 0000000000000..e0610a8caf8e8
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json
@@ -0,0 +1,2908 @@
+{
+ "total": 850,
+ "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000706686\"],\"ChargeTypeCode\":[\"46\"],\"Note\":[\"Nettarif C time\"]}",
+ "limit": 100,
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-04-30T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-29T00:00:00",
+ "ValidTo": "2023-04-30T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-28T00:00:00",
+ "ValidTo": "2023-04-29T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-27T00:00:00",
+ "ValidTo": "2023-04-28T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-26T00:00:00",
+ "ValidTo": "2023-04-27T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-25T00:00:00",
+ "ValidTo": "2023-04-26T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-24T00:00:00",
+ "ValidTo": "2023-04-25T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-23T00:00:00",
+ "ValidTo": "2023-04-24T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-22T00:00:00",
+ "ValidTo": "2023-04-23T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-21T00:00:00",
+ "ValidTo": "2023-04-22T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-20T00:00:00",
+ "ValidTo": "2023-04-21T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-19T00:00:00",
+ "ValidTo": "2023-04-20T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-18T00:00:00",
+ "ValidTo": "2023-04-19T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-17T00:00:00",
+ "ValidTo": "2023-04-18T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-16T00:00:00",
+ "ValidTo": "2023-04-17T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-15T00:00:00",
+ "ValidTo": "2023-04-16T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-14T00:00:00",
+ "ValidTo": "2023-04-15T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-13T00:00:00",
+ "ValidTo": "2023-04-14T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-12T00:00:00",
+ "ValidTo": "2023-04-13T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-11T00:00:00",
+ "ValidTo": "2023-04-12T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-10T00:00:00",
+ "ValidTo": "2023-04-11T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-09T00:00:00",
+ "ValidTo": "2023-04-10T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-08T00:00:00",
+ "ValidTo": "2023-04-09T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-07T00:00:00",
+ "ValidTo": "2023-04-08T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-06T00:00:00",
+ "ValidTo": "2023-04-07T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-05T00:00:00",
+ "ValidTo": "2023-04-06T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-04T00:00:00",
+ "ValidTo": "2023-04-05T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-03T00:00:00",
+ "ValidTo": "2023-04-04T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-02T00:00:00",
+ "ValidTo": "2023-04-03T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-04-01T00:00:00",
+ "ValidTo": "2023-04-02T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.3871,
+ "Price8": 0.3871,
+ "Price9": 0.3871,
+ "Price10": 0.3871,
+ "Price11": 0.3871,
+ "Price12": 0.3871,
+ "Price13": 0.3871,
+ "Price14": 0.3871,
+ "Price15": 0.3871,
+ "Price16": 0.3871,
+ "Price17": 0.3871,
+ "Price18": 1.0065,
+ "Price19": 1.0065,
+ "Price20": 1.0065,
+ "Price21": 1.0065,
+ "Price22": 0.3871,
+ "Price23": 0.3871,
+ "Price24": 0.3871
+ },
+ {
+ "ValidFrom": "2023-03-31T00:00:00",
+ "ValidTo": "2023-04-01T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-30T00:00:00",
+ "ValidTo": "2023-03-31T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-29T00:00:00",
+ "ValidTo": "2023-03-30T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-28T00:00:00",
+ "ValidTo": "2023-03-29T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-27T00:00:00",
+ "ValidTo": "2023-03-28T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-26T00:00:00",
+ "ValidTo": "2023-03-27T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-25T00:00:00",
+ "ValidTo": "2023-03-26T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-24T00:00:00",
+ "ValidTo": "2023-03-25T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-23T00:00:00",
+ "ValidTo": "2023-03-24T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-22T00:00:00",
+ "ValidTo": "2023-03-23T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-21T00:00:00",
+ "ValidTo": "2023-03-22T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-20T00:00:00",
+ "ValidTo": "2023-03-21T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-19T00:00:00",
+ "ValidTo": "2023-03-20T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-18T00:00:00",
+ "ValidTo": "2023-03-19T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-17T00:00:00",
+ "ValidTo": "2023-03-18T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-16T00:00:00",
+ "ValidTo": "2023-03-17T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-15T00:00:00",
+ "ValidTo": "2023-03-16T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-14T00:00:00",
+ "ValidTo": "2023-03-15T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-13T00:00:00",
+ "ValidTo": "2023-03-14T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-12T00:00:00",
+ "ValidTo": "2023-03-13T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-11T00:00:00",
+ "ValidTo": "2023-03-12T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-10T00:00:00",
+ "ValidTo": "2023-03-11T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-09T00:00:00",
+ "ValidTo": "2023-03-10T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-08T00:00:00",
+ "ValidTo": "2023-03-09T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-07T00:00:00",
+ "ValidTo": "2023-03-08T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-06T00:00:00",
+ "ValidTo": "2023-03-07T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-05T00:00:00",
+ "ValidTo": "2023-03-06T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-04T00:00:00",
+ "ValidTo": "2023-03-05T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-03T00:00:00",
+ "ValidTo": "2023-03-04T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-02T00:00:00",
+ "ValidTo": "2023-03-03T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-03-01T00:00:00",
+ "ValidTo": "2023-03-02T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-28T00:00:00",
+ "ValidTo": "2023-03-01T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-27T00:00:00",
+ "ValidTo": "2023-02-28T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-26T00:00:00",
+ "ValidTo": "2023-02-27T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-25T00:00:00",
+ "ValidTo": "2023-02-26T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-24T00:00:00",
+ "ValidTo": "2023-02-25T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-23T00:00:00",
+ "ValidTo": "2023-02-24T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-22T00:00:00",
+ "ValidTo": "2023-02-23T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-21T00:00:00",
+ "ValidTo": "2023-02-22T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-20T00:00:00",
+ "ValidTo": "2023-02-21T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-19T00:00:00",
+ "ValidTo": "2023-02-20T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-18T00:00:00",
+ "ValidTo": "2023-02-19T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-17T00:00:00",
+ "ValidTo": "2023-02-18T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-16T00:00:00",
+ "ValidTo": "2023-02-17T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-15T00:00:00",
+ "ValidTo": "2023-02-16T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-14T00:00:00",
+ "ValidTo": "2023-02-15T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-13T00:00:00",
+ "ValidTo": "2023-02-14T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-12T00:00:00",
+ "ValidTo": "2023-02-13T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-11T00:00:00",
+ "ValidTo": "2023-02-12T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-10T00:00:00",
+ "ValidTo": "2023-02-11T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-09T00:00:00",
+ "ValidTo": "2023-02-10T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-08T00:00:00",
+ "ValidTo": "2023-02-09T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-07T00:00:00",
+ "ValidTo": "2023-02-08T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-06T00:00:00",
+ "ValidTo": "2023-02-07T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-05T00:00:00",
+ "ValidTo": "2023-02-06T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-04T00:00:00",
+ "ValidTo": "2023-02-05T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-03T00:00:00",
+ "ValidTo": "2023-02-04T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-02T00:00:00",
+ "ValidTo": "2023-02-03T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-02-01T00:00:00",
+ "ValidTo": "2023-02-02T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-31T00:00:00",
+ "ValidTo": "2023-02-01T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-30T00:00:00",
+ "ValidTo": "2023-01-31T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-29T00:00:00",
+ "ValidTo": "2023-01-30T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-28T00:00:00",
+ "ValidTo": "2023-01-29T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-27T00:00:00",
+ "ValidTo": "2023-01-28T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-26T00:00:00",
+ "ValidTo": "2023-01-27T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-25T00:00:00",
+ "ValidTo": "2023-01-26T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-24T00:00:00",
+ "ValidTo": "2023-01-25T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-23T00:00:00",
+ "ValidTo": "2023-01-24T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-22T00:00:00",
+ "ValidTo": "2023-01-23T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ },
+ {
+ "ValidFrom": "2023-01-21T00:00:00",
+ "ValidTo": "2023-01-22T00:00:00",
+ "ChargeTypeCode": "46",
+ "Price1": 0.2581,
+ "Price2": 0.2581,
+ "Price3": 0.2581,
+ "Price4": 0.2581,
+ "Price5": 0.2581,
+ "Price6": 0.2581,
+ "Price7": 0.7742,
+ "Price8": 0.7742,
+ "Price9": 0.7742,
+ "Price10": 0.7742,
+ "Price11": 0.7742,
+ "Price12": 0.7742,
+ "Price13": 0.7742,
+ "Price14": 0.7742,
+ "Price15": 0.7742,
+ "Price16": 0.7742,
+ "Price17": 0.7742,
+ "Price18": 2.3227,
+ "Price19": 2.3227,
+ "Price20": 2.3227,
+ "Price21": 2.3227,
+ "Price22": 0.7742,
+ "Price23": 0.7742,
+ "Price24": 0.7742
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json
new file mode 100644
index 0000000000000..5ee45ea673045
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json
@@ -0,0 +1,45 @@
+{
+ "total": 1,
+ "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}",
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ChargeOwner": "Energinet Systemansvar A/S (SYO)",
+ "GLN_Number": "5790000432752",
+ "ChargeType": "D03",
+ "ChargeTypeCode": "EA-001",
+ "Note": "Elafgift",
+ "Description": "Elafgiften",
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": "2023-07-01T00:00:00",
+ "VATClass": "D02",
+ "Price1": 0.008,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null,
+ "TransparentInvoicing": 1,
+ "TaxIndicator": 1,
+ "ResolutionDuration": "P1D"
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json
new file mode 100644
index 0000000000000..1af9754e1a6b0
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json
@@ -0,0 +1,37 @@
+{
+ "total": 1,
+ "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}",
+ "limit": 100,
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": "2023-04-01T00:00:00",
+ "ChargeTypeCode": "CD",
+ "Price1": 0.432225,
+ "Price2": 0.432225,
+ "Price3": 0.432225,
+ "Price4": 0.432225,
+ "Price5": 0.432225,
+ "Price6": 0.432225,
+ "Price7": 0.432225,
+ "Price8": 0.432225,
+ "Price9": 0.432225,
+ "Price10": 0.432225,
+ "Price11": 0.432225,
+ "Price12": 0.432225,
+ "Price13": 0.432225,
+ "Price14": 0.432225,
+ "Price15": 0.432225,
+ "Price16": 0.432225,
+ "Price17": 0.432225,
+ "Price18": 1.05619,
+ "Price19": 1.05619,
+ "Price20": 1.05619,
+ "Price21": 0.432225,
+ "Price22": 0.432225,
+ "Price23": 0.432225,
+ "Price24": 0.432225
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json
new file mode 100644
index 0000000000000..76423da41996e
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json
@@ -0,0 +1,142 @@
+[
+ {
+ "hourStart": "2023-02-04T12:00:00Z",
+ "spotPrice": 0.992840027
+ },
+ {
+ "hourStart": "2023-02-04T13:00:00Z",
+ "spotPrice": 0.998200012
+ },
+ {
+ "hourStart": "2023-02-04T14:00:00Z",
+ "spotPrice": 1.054180054
+ },
+ {
+ "hourStart": "2023-02-04T15:00:00Z",
+ "spotPrice": 1.156540039
+ },
+ {
+ "hourStart": "2023-02-04T16:00:00Z",
+ "spotPrice": 1.267680054
+ },
+ {
+ "hourStart": "2023-02-04T17:00:00Z",
+ "spotPrice": 1.370939941
+ },
+ {
+ "hourStart": "2023-02-04T18:00:00Z",
+ "spotPrice": 1.339670044
+ },
+ {
+ "hourStart": "2023-02-04T19:00:00Z",
+ "spotPrice": 1.24973999
+ },
+ {
+ "hourStart": "2023-02-04T20:00:00Z",
+ "spotPrice": 1.177160034
+ },
+ {
+ "hourStart": "2023-02-04T21:00:00Z",
+ "spotPrice": 0.979809998
+ },
+ {
+ "hourStart": "2023-02-04T22:00:00Z",
+ "spotPrice": 0.804200012
+ },
+ {
+ "hourStart": "2023-02-04T23:00:00Z",
+ "spotPrice": 0.82826001
+ },
+ {
+ "hourStart": "2023-02-05T00:00:00Z",
+ "spotPrice": 0.777280029
+ },
+ {
+ "hourStart": "2023-02-05T01:00:00Z",
+ "spotPrice": 0.771549988
+ },
+ {
+ "hourStart": "2023-02-05T02:00:00Z",
+ "spotPrice": 0.757559998
+ },
+ {
+ "hourStart": "2023-02-05T03:00:00Z",
+ "spotPrice": 0.751599976
+ },
+ {
+ "hourStart": "2023-02-05T04:00:00Z",
+ "spotPrice": 0.76373999
+ },
+ {
+ "hourStart": "2023-02-05T05:00:00Z",
+ "spotPrice": 0.764700012
+ },
+ {
+ "hourStart": "2023-02-05T06:00:00Z",
+ "spotPrice": 0.784650024
+ },
+ {
+ "hourStart": "2023-02-05T07:00:00Z",
+ "spotPrice": 0.79551001
+ },
+ {
+ "hourStart": "2023-02-05T08:00:00Z",
+ "spotPrice": 0.805789978
+ },
+ {
+ "hourStart": "2023-02-05T09:00:00Z",
+ "spotPrice": 0.807789978
+ },
+ {
+ "hourStart": "2023-02-05T10:00:00Z",
+ "spotPrice": 0.796849976
+ },
+ {
+ "hourStart": "2023-02-05T11:00:00Z",
+ "spotPrice": 0.756289978
+ },
+ {
+ "hourStart": "2023-02-05T12:00:00Z",
+ "spotPrice": 0.749369995
+ },
+ {
+ "hourStart": "2023-02-05T13:00:00Z",
+ "spotPrice": 0.7915
+ },
+ {
+ "hourStart": "2023-02-05T14:00:00Z",
+ "spotPrice": 0.838830017
+ },
+ {
+ "hourStart": "2023-02-05T15:00:00Z",
+ "spotPrice": 0.892859985
+ },
+ {
+ "hourStart": "2023-02-05T16:00:00Z",
+ "spotPrice": 1.01997998
+ },
+ {
+ "hourStart": "2023-02-05T17:00:00Z",
+ "spotPrice": 0.99452002
+ },
+ {
+ "hourStart": "2023-02-05T18:00:00Z",
+ "spotPrice": 0.976140015
+ },
+ {
+ "hourStart": "2023-02-05T19:00:00Z",
+ "spotPrice": 0.923669983
+ },
+ {
+ "hourStart": "2023-02-05T20:00:00Z",
+ "spotPrice": 0.906700012
+ },
+ {
+ "hourStart": "2023-02-05T21:00:00Z",
+ "spotPrice": 0.931859985
+ },
+ {
+ "hourStart": "2023-02-05T22:00:00Z",
+ "spotPrice": 0.941159973
+ }
+]
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json
new file mode 100644
index 0000000000000..ce12925c942eb
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json
@@ -0,0 +1,142 @@
+[
+ {
+ "hourStart": "2023-02-05T12:00:00Z",
+ "spotPrice": 0.749609985
+ },
+ {
+ "hourStart": "2023-02-05T13:00:00Z",
+ "spotPrice": 0.79173999
+ },
+ {
+ "hourStart": "2023-02-05T14:00:00Z",
+ "spotPrice": 0.839090027
+ },
+ {
+ "hourStart": "2023-02-05T15:00:00Z",
+ "spotPrice": 0.893140015
+ },
+ {
+ "hourStart": "2023-02-05T16:00:00Z",
+ "spotPrice": 1.020299988
+ },
+ {
+ "hourStart": "2023-02-05T17:00:00Z",
+ "spotPrice": 0.994840027
+ },
+ {
+ "hourStart": "2023-02-05T18:00:00Z",
+ "spotPrice": 0.976450012
+ },
+ {
+ "hourStart": "2023-02-05T19:00:00Z",
+ "spotPrice": 0.923960022
+ },
+ {
+ "hourStart": "2023-02-05T20:00:00Z",
+ "spotPrice": 0.90698999
+ },
+ {
+ "hourStart": "2023-02-05T21:00:00Z",
+ "spotPrice": 0.932150024
+ },
+ {
+ "hourStart": "2023-02-05T22:00:00Z",
+ "spotPrice": 0.941460022
+ },
+ {
+ "hourStart": "2023-02-05T23:00:00Z",
+ "spotPrice": 1.07947998
+ },
+ {
+ "hourStart": "2023-02-06T00:00:00Z",
+ "spotPrice": 1.070030029
+ },
+ {
+ "hourStart": "2023-02-06T01:00:00Z",
+ "spotPrice": 1.082540039
+ },
+ {
+ "hourStart": "2023-02-06T02:00:00Z",
+ "spotPrice": 1.057819946
+ },
+ {
+ "hourStart": "2023-02-06T03:00:00Z",
+ "spotPrice": 1.0430
+ },
+ {
+ "hourStart": "2023-02-06T04:00:00Z",
+ "spotPrice": 1.10873999
+ },
+ {
+ "hourStart": "2023-02-06T05:00:00Z",
+ "spotPrice": 1.307810059
+ },
+ {
+ "hourStart": "2023-02-06T06:00:00Z",
+ "spotPrice": 1.493780029
+ },
+ {
+ "hourStart": "2023-02-06T07:00:00Z",
+ "spotPrice": 1.588630005
+ },
+ {
+ "hourStart": "2023-02-06T08:00:00Z",
+ "spotPrice": 1.493780029
+ },
+ {
+ "hourStart": "2023-02-06T09:00:00Z",
+ "spotPrice": 1.377869995
+ },
+ {
+ "hourStart": "2023-02-06T10:00:00Z",
+ "spotPrice": 1.338859985
+ },
+ {
+ "hourStart": "2023-02-06T11:00:00Z",
+ "spotPrice": 1.256069946
+ },
+ {
+ "hourStart": "2023-02-06T12:00:00Z",
+ "spotPrice": 1.199790039
+ },
+ {
+ "hourStart": "2023-02-06T13:00:00Z",
+ "spotPrice": 1.220189941
+ },
+ {
+ "hourStart": "2023-02-06T14:00:00Z",
+ "spotPrice": 1.270589966
+ },
+ {
+ "hourStart": "2023-02-06T15:00:00Z",
+ "spotPrice": 1.353449951
+ },
+ {
+ "hourStart": "2023-02-06T16:00:00Z",
+ "spotPrice": 1.481050049
+ },
+ {
+ "hourStart": "2023-02-06T17:00:00Z",
+ "spotPrice": 1.589449951
+ },
+ {
+ "hourStart": "2023-02-06T18:00:00Z",
+ "spotPrice": 1.52898999
+ },
+ {
+ "hourStart": "2023-02-06T19:00:00Z",
+ "spotPrice": 1.386280029
+ },
+ {
+ "hourStart": "2023-02-06T20:00:00Z",
+ "spotPrice": 1.239400024
+ },
+ {
+ "hourStart": "2023-02-06T21:00:00Z",
+ "spotPrice": 1.135319946
+ },
+ {
+ "hourStart": "2023-02-06T22:00:00Z",
+ "spotPrice": 1.14648999
+ }
+]
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json
new file mode 100644
index 0000000000000..8b1d42b98391f
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json
@@ -0,0 +1,36 @@
+{
+ "total": 1,
+ "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Systemtarif\"]}",
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "41000",
+ "Price1": 0.054,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null
+ }
+ ]
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json
new file mode 100644
index 0000000000000..c2ed993403483
--- /dev/null
+++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json
@@ -0,0 +1,36 @@
+{
+ "total": 1,
+ "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Transmissions nettarif\"]}",
+ "dataset": "DatahubPricelist",
+ "records": [
+ {
+ "ValidFrom": "2023-01-01T00:00:00",
+ "ValidTo": null,
+ "ChargeTypeCode": "40000",
+ "Price1": 0.058,
+ "Price2": null,
+ "Price3": null,
+ "Price4": null,
+ "Price5": null,
+ "Price6": null,
+ "Price7": null,
+ "Price8": null,
+ "Price9": null,
+ "Price10": null,
+ "Price11": null,
+ "Price12": null,
+ "Price13": null,
+ "Price14": null,
+ "Price15": null,
+ "Price16": null,
+ "Price17": null,
+ "Price18": null,
+ "Price19": null,
+ "Price20": null,
+ "Price21": null,
+ "Price22": null,
+ "Price23": null,
+ "Price24": null
+ }
+ ]
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 7ceb398af4889..67d24db800c76 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -119,6 +119,7 @@
org.openhab.binding.elerotransmitterstick
org.openhab.binding.elroconnects
org.openhab.binding.energenie
+ org.openhab.binding.energidataservice
org.openhab.binding.enigma2
org.openhab.binding.enocean
org.openhab.binding.enphase