]> git.basschouten.com Git - openhab-addons.git/commitdiff
[energidataservice] Initial contribution (#14376)
authorJacob Laursen <jacob-github@vindvejr.dk>
Mon, 3 Jul 2023 16:16:17 +0000 (18:16 +0200)
committerGitHub <noreply@github.com>
Mon, 3 Jul 2023 16:16:17 +0000 (18:16 +0200)
* Initial contribution

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Remove Value-Added Tax

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Migrate naming convention

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add channel configuration example

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Remove current prefixes for forward compatibility with timestamped items

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add filter for another grid company

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Use ISO 3166-1 alpha-2 codes in lowercase for XSD compliance

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix error handling for deserializers

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix compliance with RFC 9110 section 10.1.5

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add JavaScript example code

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Refactor List to Collection and use iterators

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add filter for another grid company

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Extend cached history to 24 hours

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Remove filter for expired GLN

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix typos

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Improve descriptions

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Improve logging

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
---------

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
64 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.energidataservice/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/README.md [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json [new file with mode: 0644]
bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json [new file with mode: 0644]
bundles/pom.xml

index c737e7c7ef09d237e154f40d10a299ec3e4ca829..a6d6e160d38584138cbb377c77ccceb57cd844a6 100644 (file)
@@ -92,6 +92,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
index 54161aed4e1c26c770630985f092ba64afca579c..2a48a1987d3a1f3c8b4540752e56b7af643b10fc 100644 (file)
       <artifactId>org.openhab.binding.energenie</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.energidataservice</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.enigma2</artifactId>
diff --git a/bundles/org.openhab.binding.energidataservice/NOTICE b/bundles/org.openhab.binding.energidataservice/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..41bcbe7
--- /dev/null
@@ -0,0 +1,472 @@
+# 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 |
+|-------------------------|--------|---------------------------------------------------------------------------------------|----------|
+| spot-price              | Number | Current spot price in DKK or EUR per kWh                                              | no       |
+| net-tariff              | Number | Current net tariff in DKK per kWh. Only available when `gridCompanyGLN` is configured | no       |
+| system-tariff           | Number | Current system tariff in DKK per kWh                                                  | no       |
+| electricity-tax         | Number | Current electricity tax in DKK per kWh                                                | no       |
+| transmission-net-tariff | Number | Current transmission net tariff in DKK per kWh                                        | no       |
+| hourly-prices           | 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
+
+VAT is not included in any of the prices.
+To include VAT for items linked to the `Number` channels, the [VAT profile](https://www.openhab.org/addons/transformations/vat/) can be used.
+This must be installed separately.
+Once installed, simply select "Value-Added Tax" as Profile when linking an item.
+
+#### Net Tariff
+
+Discounts are automatically taken into account for channel `net-tariff` 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 `net-tariff` 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 the 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 `hourly-prices` 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.
+
+## Thing Actions
+
+Thing actions can be used to perform calculations as well as import prices directly into rules without deserializing JSON from the [hourly-prices](#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 individual channels to items, or by linking the `hourly-prices` 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.
+For this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`.
+
+Example:
+
+```javascript
+var Map<String, Object> 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<Power>`       | Linear power consumption                               |
+
+This action is identical to 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<String, Object> 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<Duration>`            | List of durations for the phases                       |
+| powerPhases        | `List<QuantityType<Power>>` | 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<Power>` objects for that duration of time.
+
+Example:
+
+```javascript
+val ArrayList<Duration> durationPhases = new ArrayList<Duration>()
+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<QuantityType<Power>> powerPhases = new ArrayList<QuantityType<Power>>()
+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<String, Object> 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<Duration>`            | List of durations for the phases                       |
+| energyUsedPerPhase | `QuantityType<Energy>`      | 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<Duration> durationPhases = new ArrayList<Duration>()
+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<String, Object> 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<Power>`       | 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<Instant, BigDecimal>`
+
+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#net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ]
+}
+```
+
+### Item Configuration
+
+```java
+Group:Number:SUM TotalPrice "Current Total Price" <price>
+Number SpotPrice "Current Spot Price" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#spot-price" [profile="transform:VAT"] }
+Number NetTariff "Current Net Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#net-tariff" [profile="transform:VAT"] }
+Number SystemTariff "Current System Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#system-tariff" [profile="transform:VAT"] }
+Number ElectricityTax "Current Electricity Tax" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#electricity-tax" [profile="transform:VAT"] }
+Number TransmissionNetTariff "Current Transmission Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#transmission-net-tariff" [profile="transform:VAT"] }
+String HourlyPrices "Hourly Prices" <price> { channel="energidataservice:service:energidataservice:electricity#hourly-prices" }
+```
+
+### Thing Actions Example
+
+:::: tabs
+
+::: tab DSL
+
+```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<Duration> durationPhases = new ArrayList<Duration>()
+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<QuantityType<Power>> consumptionPhases = new ArrayList<QuantityType<Power>>()
+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<String, Object> result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases)
+logInfo("Cheapest start", (result.get("CheapestStart") as Instant).toString)
+logInfo("Lowest price", (result.get("LowestPrice") as Number).doubleValue.toString)
+logInfo("Highest price", (result.get("HighestPrice") as Number).doubleValue.toString)
+logInfo("Most expensive start", (result.get("MostExpensiveStart") as Instant).toString)
+
+// 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<Duration> durationPhases = new ArrayList<Duration>()
+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))
+
+var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), durationPhases, 0.1 | kWh)
+```
+
+:::
+
+::: tab JavaScript
+
+```javascript
+var edsActions = actions.get("energidataservice", "energidataservice:service:energidataservice");
+
+// Get prices and convert to JavaScript Map with Instant string representation as keys.
+var priceMap = new Map();
+utils.javaMapToJsMap(edsActions.getPrices()).forEach((value, key) => {
+    priceMap.set(key.toString(), value);
+});
+
+var hourStart = time.Instant.now().truncatedTo(time.ChronoUnit.HOURS);
+console.log("Current total price excl. VAT: " + priceMap.get(hourStart.toString()));
+
+utils.javaMapToJsMap(edsActions.getPrices("SpotPrice,NetTariff")).forEach((value, key) => {
+    priceMap.set(key.toString(), value);
+});
+console.log("Current spot price + net tariff excl. VAT: " + priceMap.get(hourStart.toString()));
+
+var price = edsActions.calculatePrice(time.Instant.now(), time.Instant.now().plusSeconds(3600), Quantity("150 W"));
+console.log("Total price for using 150 W for the next hour: " + price.toString());
+
+var durationPhases = [];
+durationPhases.push(time.Duration.ofMinutes(37));
+durationPhases.push(time.Duration.ofMinutes(8));
+durationPhases.push(time.Duration.ofMinutes(4));
+durationPhases.push(time.Duration.ofMinutes(2));
+durationPhases.push(time.Duration.ofMinutes(4));
+durationPhases.push(time.Duration.ofMinutes(36));
+durationPhases.push(time.Duration.ofMinutes(41));
+durationPhases.push(time.Duration.ofMinutes(104));
+
+var consumptionPhases = [];
+consumptionPhases.push(Quantity("162.162 W"));
+consumptionPhases.push(Quantity("750 W"));
+consumptionPhases.push(Quantity("1500 W"));
+consumptionPhases.push(Quantity("3000 W"));
+consumptionPhases.push(Quantity("1500 W"));
+consumptionPhases.push(Quantity("166.666 W"));
+consumptionPhases.push(Quantity("146.341 W"));
+consumptionPhases.push(Quantity("0 W"));
+
+var result = edsActions.calculateCheapestPeriod(time.Instant.now(), time.Instant.now().plusSeconds(24*60*60), durationPhases, consumptionPhases);
+
+console.log("Cheapest start: " + result.get("CheapestStart").toString());
+console.log("Lowest price: " + result.get("LowestPrice"));
+console.log("Highest price: " + result.get("HighestPrice"));
+console.log("Most expensive start: " + result.get("MostExpensiveStart").toString());
+
+// 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.
+var durationPhases = [];
+durationPhases.push(time.Duration.ofMinutes(37));
+durationPhases.push(time.Duration.ofMinutes(8));
+durationPhases.push(time.Duration.ofMinutes(4));
+durationPhases.push(time.Duration.ofMinutes(2));
+durationPhases.push(time.Duration.ofMinutes(4));
+durationPhases.push(time.Duration.ofMinutes(36));
+durationPhases.push(time.Duration.ofMinutes(41));
+
+var result = edsActions.calculateCheapestPeriod(time.Instant.now(), time.Instant.now().plusSeconds(24*60*60), time.Duration.ofMinutes(236), durationPhases, Quantity("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 (file)
index 0000000..4e4d39f
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.energidataservice</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Energi Data Service Binding</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.10.1</version>
+    </dependency>
+  </dependencies>
+
+</project>
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 (file)
index 0000000..69ed1af
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.energidataservice-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-energidataservice" description="Energi Data Service Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.energidataservice/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..484986f
--- /dev/null
@@ -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<String, String> properties) throws InterruptedException, DataServiceException {
+        if (!SUPPORTED_CURRENCIES.contains(currency)) {
+            throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
+        }
+
+        Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_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<String, String> 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<DatahubPricelistRecord> getDatahubPriceLists(GlobalLocationNumber globalLocationNumber,
+            ChargeType chargeType, DatahubTariffFilter tariffFilter, Map<String, String> properties)
+            throws InterruptedException, DataServiceException {
+        String columns = "ValidFrom,ValidTo,ChargeTypeCode";
+        for (int i = 1; i < 25; i++) {
+            columns += ",Price" + i;
+        }
+
+        Map<String, Collection<String>> filterMap = new HashMap<>(Map.of( //
+                FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), //
+                FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString())));
+
+        Collection<String> chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings();
+        if (!chargeTypeCodes.isEmpty()) {
+            filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes);
+        }
+
+        Collection<String> 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<String, Collection<String>> 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 (file)
index 0000000..d73ee12
--- /dev/null
@@ -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 static final int NUMBER_OF_HISTORIC_HOURS = 24;
+    public static final int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS;
+    public static final int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS;
+
+    private final Clock clock;
+    private final PriceListParser priceListParser = new PriceListParser();
+
+    private Collection<DatahubPricelistRecord> netTariffRecords = new ArrayList<>();
+    private Collection<DatahubPricelistRecord> systemTariffRecords = new ArrayList<>();
+    private Collection<DatahubPricelistRecord> electricityTaxRecords = new ArrayList<>();
+    private Collection<DatahubPricelistRecord> transmissionNetTariffRecords = new ArrayList<>();
+
+    private Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
+    private Map<Instant, BigDecimal> netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+    private Map<Instant, BigDecimal> systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+    private Map<Instant, BigDecimal> electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
+    private Map<Instant, BigDecimal> 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<DatahubPricelistRecord> 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<DatahubPricelistRecord> 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<DatahubPricelistRecord> 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<DatahubPricelistRecord> records) {
+        putDatahubRecords(transmissionNetTariffRecords, records);
+        updateTransmissionNetTariffs();
+    }
+
+    private void putDatahubRecords(Collection<DatahubPricelistRecord> destination,
+            Collection<DatahubPricelistRecord> 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<Instant, BigDecimal> getSpotPrices() {
+        return new HashMap<Instant, BigDecimal>(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<Instant, BigDecimal> getNetTariffs() {
+        return new HashMap<Instant, BigDecimal>(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<Instant, BigDecimal> getSystemTariffs() {
+        return new HashMap<Instant, BigDecimal>(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<Instant, BigDecimal> getElectricityTaxes() {
+        return new HashMap<Instant, BigDecimal>(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<Instant, BigDecimal> getTransmissionNetTariffs() {
+        return new HashMap<Instant, BigDecimal>(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<Instant, BigDecimal> 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<DatahubPricelistRecord> 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 (file)
index 0000000..aa82862
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * 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_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+            + "spot-price";
+    public static final String CHANNEL_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+            + "net-tariff";
+    public static final String CHANNEL_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+            + "system-tariff";
+    public static final String CHANNEL_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+            + "electricity-tax";
+    public static final String CHANNEL_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY
+            + ChannelUID.CHANNEL_GROUP_SEPARATOR + "transmission-net-tariff";
+    public static final String CHANNEL_HOURLY_PRICES = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
+            + "hourly-prices";
+
+    public static final Set<String> ELECTRICITY_CHANNELS = Set.of(CHANNEL_SPOT_PRICE, CHANNEL_NET_TARIFF,
+            CHANNEL_SYSTEM_TARIFF, CHANNEL_ELECTRICITY_TAX, CHANNEL_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<Currency> 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 (file)
index 0000000..67e9889
--- /dev/null
@@ -0,0 +1,238 @@
+/**
+ * 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.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+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<Instant, BigDecimal> priceMap;
+
+    public PriceCalculator(Map<Instant, BigDecimal> 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<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration,
+            Collection<Duration> durationPhases, QuantityType<Energy> energyUsedPerPhase) throws MissingPriceException {
+        QuantityType<Energy> 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<QuantityType<Power>> 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<Duration> 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<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration,
+            QuantityType<Power> 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<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd,
+            Collection<Duration> durationPhases, Collection<QuantityType<Power>> consumptionPhases)
+            throws MissingPriceException {
+        if (durationPhases.size() != consumptionPhases.size()) {
+            throw new IllegalArgumentException("Number of phases do not match");
+        }
+        Map<String, Object> 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;
+
+            Iterator<Duration> durationIterator = durationPhases.iterator();
+            Iterator<QuantityType<Power>> consumptionIterator = consumptionPhases.iterator();
+            while (durationIterator.hasNext()) {
+                Duration atomDuration = durationIterator.next();
+                QuantityType<Power> atomConsumption = consumptionIterator.next();
+
+                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, atomConsumption);
+                currentPrice = currentPrice.add(atomPrice);
+                atomStart = atomEnd;
+            }
+
+            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> power)
+            throws MissingPriceException {
+        QuantityType<Power> 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 (file)
index 0000000..5f4bedc
--- /dev/null
@@ -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<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records) {
+        Map<Instant, BigDecimal> totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
+        records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> {
+            Map<Instant, BigDecimal> currentMap = toHourly(records, chargeTypeCode);
+            for (Entry<Instant, BigDecimal> 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<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records, String chargeTypeCode) {
+        Map<Instant, BigDecimal> tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
+
+        Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)
+                .truncatedTo(ChronoUnit.HOURS);
+        Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS)
+                .truncatedTo(ChronoUnit.DAYS);
+
+        LocalDateTime previousValidFrom = LocalDateTime.MAX;
+        LocalDateTime previousValidTo = LocalDateTime.MIN;
+        Map<LocalTime, BigDecimal> tariffs = Map.of();
+        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<DatahubPricelistRecord> 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 (file)
index 0000000..a39897d
--- /dev/null
@@ -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<String, PriceElement> 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<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> 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<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> 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<PriceElement> priceElementsSet;
+        try {
+            priceElementsSet = new HashSet<PriceElement>(
+                    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<Power>") QuantityType<Power> 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<String, Object>") Map<String, Object> 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<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
+                    duration, QuantityType.valueOf(1000, Units.WATT));
+
+            // Create new result with stripped price information.
+            Map<String, Object> 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<String, Object>") Map<String, Object> 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<Power>") QuantityType<Power> 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<String, Object>") Map<String, Object> 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<java.time.Duration>") List<Duration> durationPhases,
+            @ActionInput(name = "energyUsedPerPhase", type = "QuantityType<Energy>") QuantityType<Energy> 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<String, Object>") Map<String, Object> 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<java.time.Duration>") List<Duration> durationPhases,
+            @ActionInput(name = "powerPhases", type = "java.util.List<QuantityType<Power>>") List<QuantityType<Power>> 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<Instant, BigDecimal> getPrices(Set<PriceElement> priceElements) {
+        EnergiDataServiceHandler handler = this.handler;
+        if (handler == null) {
+            logger.warn("EnergiDataServiceActions ThingHandler is null.");
+            return Map.of();
+        }
+
+        Map<Instant, BigDecimal> 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<Instant, BigDecimal> netTariffMap = handler.getNetTariffs();
+            mergeMaps(prices, netTariffMap, !spotPricesRequired);
+        }
+
+        if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
+            Map<Instant, BigDecimal> systemTariffMap = handler.getSystemTariffs();
+            mergeMaps(prices, systemTariffMap, !spotPricesRequired);
+        }
+
+        if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
+            Map<Instant, BigDecimal> electricityTaxMap = handler.getElectricityTaxes();
+            mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
+        }
+
+        if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) {
+            Map<Instant, BigDecimal> transmissionNetTariffMap = handler.getTransmissionNetTariffs();
+            mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired);
+        }
+
+        return prices;
+    }
+
+    private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
+            boolean createNew) {
+        for (Entry<Instant, BigDecimal> 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<Instant, BigDecimal> 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> 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<String, Object> 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<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
+            @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration,
+            @Nullable QuantityType<Power> 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<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
+            @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration,
+            @Nullable List<Duration> durationPhases, @Nullable QuantityType<Energy> 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<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
+            @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List<Duration> durationPhases,
+            @Nullable List<QuantityType<Power>> 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 (file)
index 0000000..19d242b
--- /dev/null
@@ -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 (file)
index 0000000..b9228a1
--- /dev/null
@@ -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 (file)
index 0000000..ecbec4a
--- /dev/null
@@ -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<ChargeTypeCode> chargeTypeCodes;
+    private final Set<String> notes;
+    private final DateQueryParameter dateQueryParameter;
+
+    public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) {
+        this(filter.chargeTypeCodes, filter.notes, dateQueryParameter);
+    }
+
+    public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes) {
+        this(chargeTypeCodes, notes, DateQueryParameter.EMPTY);
+    }
+
+    public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes,
+            DateQueryParameter dateQueryParameter) {
+        this.chargeTypeCodes = chargeTypeCodes;
+        this.notes = notes;
+        this.dateQueryParameter = dateQueryParameter;
+    }
+
+    public Collection<String> getChargeTypeCodesAsStrings() {
+        return chargeTypeCodes.stream().map(c -> c.toString()).toList();
+    }
+
+    public Collection<String> 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 (file)
index 0000000..2809c15
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * 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_MIDTFYNS_ELFORSYNING = "5790001089023";
+    private static final String GLN_N1 = "5790001089030";
+    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_SUNDS_NET = "5790001095444";
+    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_NET_TARIFF_C_FLEX = NOTE_NET_TARIFF_C + " Flex";
+    private static final String NOTE_NET_TARIFF_C_FLEX_HOUR = NOTE_NET_TARIFF_C_FLEX + " - 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, 2, 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"), ChargeTypeCode.of("151-NRA04T")),
+                        Set.of(), 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_MIDTFYNS_ELFORSYNING:
+                return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TNT15000")), Set.of(NOTE_NET_TARIFF_C_FLEX),
+                        DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
+            case GLN_N1:
+                return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD"), ChargeTypeCode.of("CD R")), Set.of(),
+                        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_SUNDS_NET:
+                return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("SEF-NT-05")),
+                        Set.of(NOTE_NET_TARIFF_C_FLEX_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 (file)
index 0000000..70f996a
--- /dev/null
@@ -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 (file)
index 0000000..3d951b2
--- /dev/null
@@ -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 (file)
index 0000000..4baa897
--- /dev/null
@@ -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 (file)
index 0000000..4cbae11
--- /dev/null
@@ -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<LocalTime, BigDecimal> getTariffMap() {
+        Map<LocalTime, BigDecimal> 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 (file)
index 0000000..4aa6c28
--- /dev/null
@@ -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 (file)
index 0000000..a657471
--- /dev/null
@@ -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 (file)
index 0000000..87c8c2d
--- /dev/null
@@ -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 (file)
index 0000000..0591540
--- /dev/null
@@ -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.serialization;
+
+import java.lang.reflect.Type;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+
+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<Instant> {
+
+    @Override
+    public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+            throws JsonParseException {
+        String content = element.getAsString();
+        try {
+            // 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");
+        } catch (DateTimeParseException e) {
+            throw new JsonParseException("Could not parse as Instant: " + content, e);
+        }
+    }
+}
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 (file)
index 0000000..76f0b1f
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * 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 java.time.format.DateTimeParseException;
+
+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<LocalDate> {
+
+    @Override
+    public @Nullable LocalDate deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+            throws JsonParseException {
+        try {
+            return LocalDate.parse(element.getAsString().substring(0, 10));
+        } catch (DateTimeParseException e) {
+            throw new JsonParseException("Could not parse as LocalDate: " + element.getAsString(), e);
+        }
+    }
+}
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 (file)
index 0000000..81b2458
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * 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 java.time.format.DateTimeParseException;
+
+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<LocalDateTime> {
+
+    @Override
+    public @Nullable LocalDateTime deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
+            throws JsonParseException {
+        try {
+            return LocalDateTime.parse(element.getAsString());
+        } catch (DateTimeParseException e) {
+            throw new JsonParseException("Could not parse as LocalDateTime: " + element.getAsString(), e);
+        }
+    }
+}
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 (file)
index 0000000..8a59da8
--- /dev/null
@@ -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 {
+
+    /**
+     * 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<ChargeTypeCode> getChargeTypeCodes() {
+        return chargeTypeCodes.isBlank() ? new HashSet<>()
+                : new HashSet<ChargeTypeCode>(
+                        Arrays.stream(chargeTypeCodes.split(",")).map(ChargeTypeCode::new).toList());
+    }
+
+    /**
+     * Get parsed set of notes from comma-separated string.
+     *
+     * @return Set of notes.
+     */
+    public Set<String> getNotes() {
+        return notes.isBlank() ? new HashSet<>() : new HashSet<String>(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 (file)
index 0000000..70cf0b4
--- /dev/null
@@ -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/exception/DataServiceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java
new file mode 100644 (file)
index 0000000..ab2c522
--- /dev/null
@@ -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 (file)
index 0000000..95814c2
--- /dev/null
@@ -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 (file)
index 0000000..4f2f4ef
--- /dev/null
@@ -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.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.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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE);
+
+    private final HttpClient httpClient;
+    private final TimeZoneProvider timeZoneProvider;
+
+    @Activate
+    public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+            final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) {
+        super.activate(componentContext);
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+        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, 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 (file)
index 0000000..16bffcf
--- /dev/null
@@ -0,0 +1,541 @@
+/**
+ * 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.exception.DataServiceException;
+import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory;
+import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
+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 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, TimeZoneProvider timeZoneProvider) {
+        super(thing);
+        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<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(EnergiDataServiceActions.class);
+    }
+
+    private void refreshElectricityPrices() {
+        RetryStrategy retryPolicy;
+        try {
+            if (isLinked(CHANNEL_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) {
+                downloadSpotPrices();
+            }
+
+            if (isLinked(CHANNEL_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+                downloadNetTariffs();
+            }
+
+            if (isLinked(CHANNEL_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+                downloadSystemTariffs();
+            }
+
+            if (isLinked(CHANNEL_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) {
+                downloadElectricityTaxes();
+            }
+
+            if (isLinked(CHANNEL_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
+                downloadTransmissionNetTariffs();
+            }
+
+            updateStatus(ThingStatus.ONLINE);
+            updatePrices();
+
+            if (isLinked(CHANNEL_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<String, String> 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<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber,
+            DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
+        Map<String, String> properties = editProperties();
+        Collection<DatahubPricelistRecord> records = apiController.getDatahubPriceLists(globalLocationNumber,
+                ChargeType.Tariff, filter, properties);
+        updateProperties(properties);
+
+        return records;
+    }
+
+    private DatahubTariffFilter getNetTariffFilter() {
+        Channel channel = getThing().getChannel(CHANNEL_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<ChargeTypeCode> chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes();
+        Set<String> 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_NET_TARIFF, cacheManager.getNetTariff());
+        updateCurrentTariff(CHANNEL_SYSTEM_TARIFF, cacheManager.getSystemTariff());
+        updateCurrentTariff(CHANNEL_ELECTRICITY_TAX, cacheManager.getElectricityTax());
+        updateCurrentTariff(CHANNEL_TRANSMISSION_NET_TARIFF, cacheManager.getTransmissionNetTariff());
+        updateHourlyPrices();
+
+        reschedulePriceUpdateJob();
+    }
+
+    private void updateCurrentSpotPrice() {
+        if (!isLinked(CHANNEL_SPOT_PRICE)) {
+            return;
+        }
+        BigDecimal spotPrice = cacheManager.getSpotPrice();
+        updateState(CHANNEL_SPOT_PRICE, spotPrice != null ? new DecimalType(spotPrice) : UnDefType.UNDEF);
+    }
+
+    private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) {
+        if (!isLinked(channelId)) {
+            return;
+        }
+        updateState(channelId, tariff != null ? new DecimalType(tariff) : UnDefType.UNDEF);
+    }
+
+    private void updateHourlyPrices() {
+        if (!isLinked(CHANNEL_HOURLY_PRICES)) {
+            return;
+        }
+        Map<Instant, BigDecimal> spotPriceMap = cacheManager.getSpotPrices();
+        Price[] targetPrices = new Price[spotPriceMap.size()];
+        List<Entry<Instant, BigDecimal>> sourcePrices = spotPriceMap.entrySet().stream()
+                .sorted(Map.Entry.comparingByKey()).toList();
+
+        int i = 0;
+        for (Entry<Instant, BigDecimal> 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<Instant, BigDecimal> getSpotPrices() {
+        try {
+            downloadSpotPrices();
+        } catch (DataServiceException e) {
+            if (logger.isDebugEnabled()) {
+                logger.warn("Error retrieving spot prices", e);
+            } else {
+                logger.warn("Error retrieving spot prices: {}", e.getMessage());
+            }
+        } 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<Instant, BigDecimal> getNetTariffs() {
+        try {
+            downloadNetTariffs();
+        } catch (DataServiceException e) {
+            if (logger.isDebugEnabled()) {
+                logger.warn("Error retrieving net tariffs", e);
+            } else {
+                logger.warn("Error retrieving net tariffs: {}", e.getMessage());
+            }
+        } 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<Instant, BigDecimal> getSystemTariffs() {
+        try {
+            downloadSystemTariffs();
+        } catch (DataServiceException e) {
+            if (logger.isDebugEnabled()) {
+                logger.warn("Error retrieving system tariffs", e);
+            } else {
+                logger.warn("Error retrieving system tariffs: {}", e.getMessage());
+            }
+        } 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<Instant, BigDecimal> getElectricityTaxes() {
+        try {
+            downloadElectricityTaxes();
+        } catch (DataServiceException e) {
+            if (logger.isDebugEnabled()) {
+                logger.warn("Error retrieving electricity taxes", e);
+            } else {
+                logger.warn("Error retrieving electricity taxes: {}", e.getMessage());
+            }
+        } 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<Instant, BigDecimal> getTransmissionNetTariffs() {
+        try {
+            downloadTransmissionNetTariffs();
+        } catch (DataServiceException e) {
+            if (logger.isDebugEnabled()) {
+                logger.warn("Error retrieving transmission net tariffs", e);
+            } else {
+                logger.warn("Error retrieving transmission net tariffs: {}", e.getMessage());
+            }
+        } 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 (file)
index 0000000..00f0ab8
--- /dev/null
@@ -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 (file)
index 0000000..eb67ba8
--- /dev/null
@@ -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 (file)
index 0000000..4b510ae
--- /dev/null
@@ -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 (file)
index 0000000..98d78a0
--- /dev/null
@@ -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 (file)
index 0000000..99bd57a
--- /dev/null
@@ -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 (file)
index 0000000..70d43d2
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="energidataservice" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>Energi Data Service Binding</name>
+       <description>This is the binding for Energi Data Service providing open energy data from Energinet.</description>
+       <connection>cloud</connection>
+       <countries>dk,no,se</countries>
+</addon:addon>
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 (file)
index 0000000..77d3dfe
--- /dev/null
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:energidataservice:service">
+               <parameter name="priceArea" type="text" required="true">
+                       <label>Price Area</label>
+                       <description>Price area for spot prices (same as bidding zone).</description>
+                       <limitToOptions>false</limitToOptions>
+                       <options>
+                               <option value="DK1">West of the Great Belt</option>
+                               <option value="DK2">East of the Great Belt</option>
+                       </options>
+               </parameter>
+               <parameter name="currencyCode" type="text">
+                       <label>Currency Code</label>
+                       <description>Currency code in which to obtain spot prices.</description>
+                       <default>DKK</default>
+                       <options>
+                               <option value="DKK">Danish Krone</option>
+                               <option value="EUR">Euro</option>
+                       </options>
+               </parameter>
+               <parameter name="gridCompanyGLN" type="text">
+                       <label>Grid Company GLN</label>
+                       <description>Global Location Number of the grid company.</description>
+                       <limitToOptions>false</limitToOptions>
+                       <options>
+                               <option value="5790000705184">Cerius</option>
+                               <option value="5790000610099">Dinel</option>
+                               <option value="5790002502699">El-net Kongerslev</option>
+                               <option value="5790000836239">Elektrus</option>
+                               <option value="5790001095277">Elinord</option>
+                               <option value="5790001100520">Elnet Midt</option>
+                               <option value="5790000392551">FLOW Elnet</option>
+                               <option value="5790001090166">Hammel Elforsyning Net</option>
+                               <option value="5790000610839">Hurup Elværk Net</option>
+                               <option value="5790000682102">Ikast El Net</option>
+                               <option value="5790000704842">Konstant</option>
+                               <option value="5790001090111">L-Net</option>
+                               <option value="5790001089023">Midtfyns Elforsyning</option>
+                               <option value="5790001089030">N1</option>
+                               <option value="5790000681075">Netselskabet Elværk</option>
+                               <option value="5790001088231">NKE-Elnet</option>
+                               <option value="5790000610877">Nord Energi Net</option>
+                               <option value="5790000395620">Nordvestjysk Elforsyning (NOE Net)</option>
+                               <option value="5790000705689">Radius</option>
+                               <option value="5790000681327">RAH</option>
+                               <option value="5790000836727">Ravdex</option>
+                               <option value="5790001095444">Sunds Net</option>
+                               <option value="5790000706419">Tarm Elværk Net</option>
+                               <option value="5790000392261">TREFOR El-net</option>
+                               <option value="5790000706686">TREFOR El-net Ã˜st</option>
+                               <option value="5790001088217">Veksel</option>
+                               <option value="5790000610976">Vores Elnet</option>
+                               <option value="5790001089375">Zeanet</option>
+                       </options>
+               </parameter>
+               <parameter name="energinetGLN" type="text">
+                       <label>Energinet GLN</label>
+                       <description>Global Location Number of Energinet.</description>
+                       <advanced>true</advanced>
+                       <default>5790000432752</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:energidataservice:datahub-price">
+               <parameter name="chargeTypeCodes" type="text">
+                       <label>Charge Type Code Filters</label>
+                       <description>Comma-separated list of charge type codes.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="notes" type="text">
+                       <label>Note Filters</label>
+                       <description>Comma-separated list of notes.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="start" type="text">
+                       <label>Query Start Date</label>
+                       <description>Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay,
+                               StartOfMonth or StartOfYear.</description>
+                       <limitToOptions>false</limitToOptions>
+                       <options>
+                               <option value="StartOfDay">Start of day</option>
+                               <option value="StartOfMonth">Start of month</option>
+                               <option value="StartOfYear">Start of year</option>
+                       </options>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
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 (file)
index 0000000..8587e9c
--- /dev/null
@@ -0,0 +1,118 @@
+# add-on
+
+addon.energidataservice.name = Energi Data Service Binding
+addon.energidataservice.description = This is the binding for Energi Data Service providing open energy data from Energinet.
+
+# 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.5790001089023 = Midtfyns Elforsyning
+thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089030 = N1
+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.5790001095444 = Sunds Net
+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.electricity-tax.label = Electricity Tax
+channel-group-type.energidataservice.electricity.channel.electricity-tax.description = Current electricity tax in DKK per kWh.
+channel-group-type.energidataservice.electricity.channel.net-tariff.label = Net Tariff
+channel-group-type.energidataservice.electricity.channel.net-tariff.description = Current net tariff in DKK per kWh.
+channel-group-type.energidataservice.electricity.channel.spot-price.label = Spot Price
+channel-group-type.energidataservice.electricity.channel.spot-price.description = Current spot price in DKK or EUR per kWh.
+channel-group-type.energidataservice.electricity.channel.system-tariff.label = System Tariff
+channel-group-type.energidataservice.electricity.channel.system-tariff.description = Current system tariff in DKK per kWh.
+channel-group-type.energidataservice.electricity.channel.transmission-net-tariff.label = Transmission Net Tariff
+channel-group-type.energidataservice.electricity.channel.transmission-net-tariff.description = Current transmission net tariff in DKK per kWh.
+
+# channel types
+
+channel-type.energidataservice.datahub-price.label = Datahub Price
+channel-type.energidataservice.datahub-price.description = Datahub price.
+channel-type.energidataservice.hourly-prices.label = Hourly Prices
+channel-type.energidataservice.hourly-prices.description = JSON array with hourly prices from 12 hours ago and onward.
+channel-type.energidataservice.spot-price.label = Spot Price
+channel-type.energidataservice.spot-price.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.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 group types
+
+channel-group-type.energidataservice.electricity.channel.current-electricity-tax.label = Current Electricity Tax
+channel-group-type.energidataservice.electricity.channel.current-electricity-tax.description = Electricity Tax in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.current-net-tariff.label = Current Net Tariff
+channel-group-type.energidataservice.electricity.channel.current-net-tariff.description = Net tariff in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.current-spot-price.label = Current Spot Price
+channel-group-type.energidataservice.electricity.channel.current-spot-price.description = Spot price in DKK or EUR per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.current-system-tariff.label = Current System Tariff
+channel-group-type.energidataservice.electricity.channel.current-system-tariff.description = System tariff in DKK per kWh for current hour.
+channel-group-type.energidataservice.electricity.channel.current-transmission-net-tariff.label = Current Transmission Tariff
+channel-group-type.energidataservice.electricity.channel.current-transmission-net-tariff.description = Transmission Net Tariff in DKK per kWh for current hour.
+
+# 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 (file)
index 0000000..2a22018
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="energidataservice"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <channel-group-type id="electricity">
+               <label>Electricity</label>
+               <description>Channels related to electricity</description>
+               <channels>
+                       <channel id="spot-price" typeId="spot-price">
+                               <label>Spot Price</label>
+                               <description>Current spot price in DKK or EUR per kWh.</description>
+                       </channel>
+                       <channel id="net-tariff" typeId="datahub-price">
+                               <label>Net Tariff</label>
+                               <description>Current net tariff in DKK per kWh.</description>
+                       </channel>
+                       <channel id="system-tariff" typeId="datahub-price">
+                               <label>System Tariff</label>
+                               <description>Current system tariff in DKK per kWh.</description>
+                       </channel>
+                       <channel id="electricity-tax" typeId="datahub-price">
+                               <label>Electricity Tax</label>
+                               <description>Current electricity tax in DKK per kWh.</description>
+                       </channel>
+                       <channel id="transmission-net-tariff" typeId="datahub-price">
+                               <label>Transmission Net Tariff</label>
+                               <description>Current transmission net tariff in DKK per kWh.</description>
+                       </channel>
+                       <channel id="hourly-prices" typeId="hourly-prices"/>
+               </channels>
+       </channel-group-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..12d0273
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="energidataservice"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <channel-type id="spot-price">
+               <item-type>Number</item-type>
+               <label>Spot Price</label>
+               <description>Spot price.</description>
+               <category>Price</category>
+               <state readOnly="true" pattern="%.9f"></state>
+       </channel-type>
+
+       <channel-type id="datahub-price">
+               <item-type>Number</item-type>
+               <label>Datahub Price</label>
+               <description>Datahub price.</description>
+               <category>Price</category>
+               <state readOnly="true" pattern="%.6f"></state>
+               <config-description-ref uri="channel-type:energidataservice:datahub-price"/>
+       </channel-type>
+
+       <channel-type id="hourly-prices" advanced="true">
+               <item-type>String</item-type>
+               <label>Hourly Prices</label>
+               <description>JSON array with hourly prices from 12 hours ago and onward.</description>
+               <category>Price</category>
+               <state readOnly="true"></state>
+       </channel-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..c00115a
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="energidataservice"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="service">
+
+               <label>Energi Data Service</label>
+               <description>This Thing represents the Energi Data Service API.</description>
+
+               <channel-groups>
+                       <channel-group id="electricity" typeId="electricity"/>
+               </channel-groups>
+
+               <config-description-ref uri="thing-type:energidataservice:service"/>
+       </thing-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..a901652
--- /dev/null
@@ -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-06T08: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-06T08: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-06T12: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-06T08: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 (file)
index 0000000..da0b31a
--- /dev/null
@@ -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> T getObjectFromJson(String filename, Class<T> 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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(60));
+        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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(60));
+        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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD");
+
+        assertThat(tariffMap.size(), is(60));
+        assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717"))));
+        assertThat(tariffMap.get(Instant.parse("2022-12-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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(),
+                "CD R");
+
+        assertThat(tariffMap.size(), is(60));
+        assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717"))));
+        assertThat(tariffMap.get(Instant.parse("2022-12-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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(57));
+        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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(60));
+        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<Instant, BigDecimal> 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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(68));
+        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<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
+
+        assertThat(tariffMap.size(), is(51));
+        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 (file)
index 0000000..3170973
--- /dev/null
@@ -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> T getObjectFromJson(String filename, Class<T> 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
+    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<Instant, BigDecimal> 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<Instant, BigDecimal> actual = actions.getPrices("NetTariff");
+        assertThat(actual.size(), is(60));
+        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<Instant, BigDecimal> actual = actions.getPrices("SystemTariff");
+        assertThat(actual.size(), is(60));
+        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<Instant, BigDecimal> actual = actions.getPrices("ElectricityTax");
+        assertThat(actual.size(), is(60));
+        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<Instant, BigDecimal> actual = actions.getPrices("TransmissionNetTariff");
+        assertThat(actual.size(), is(60));
+        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<Instant, BigDecimal> 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<Instant, BigDecimal> 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<Instant, BigDecimal> 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<Instant, BigDecimal> 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<Instant, BigDecimal> 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<Instant, BigDecimal> 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<Duration> 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<QuantityType<Power>> 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<String, Object> 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<Duration> durations = List.of(Duration.ofMinutes(61));
+        List<QuantityType<Power>> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT));
+        Map<String, Object> 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<Duration> 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<String, Object> 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<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
+        Map<String, Object> 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<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
+        Map<String, Object> 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<String, Object> 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<String, Object> 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<Instant, BigDecimal> 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<Instant, BigDecimal> netTariffs = priceListParser
+                .toHourly(Arrays.stream(datahubRecords.records()).toList());
+        datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class);
+        Map<Instant, BigDecimal> systemTariffs = priceListParser
+                .toHourly(Arrays.stream(datahubRecords.records()).toList());
+        datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class);
+        Map<Instant, BigDecimal> electricityTaxes = priceListParser
+                .toHourly(Arrays.stream(datahubRecords.records()).toList());
+        datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class);
+        Map<Instant, BigDecimal> 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 (file)
index 0000000..9334b3d
--- /dev/null
@@ -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 (file)
index 0000000..15e135e
--- /dev/null
@@ -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/api/serialization/InstantDeserializerTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializerTest.java
new file mode 100644 (file)
index 0000000..92a9c23
--- /dev/null
@@ -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.serialization;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+
+/**
+ * Tests for {@link InstantDeserializer}.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class InstantDeserializerTest {
+
+    private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create();
+
+    @Test
+    void instantWhenInvalidShouldThrowJsonParseException() {
+        assertThrows(JsonParseException.class, () -> {
+            gson.fromJson("\"invalid\"", Instant.class);
+        });
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "\"2023-04-17T20:38:01Z\"", "\"2023-04-17T20:38:01\"" })
+    void instantWhenValidShouldParse(String input) {
+        assertThat((@Nullable Instant) gson.fromJson(input, Instant.class),
+                is(equalTo(Instant.ofEpochSecond(1681763881))));
+    }
+}
diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializerTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializerTest.java
new file mode 100644 (file)
index 0000000..116a8a8
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * 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 static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+
+/**
+ * Tests for {@link LocalDateDeserializer}.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class LocalDateDeserializerTest {
+
+    private final Gson gson = new GsonBuilder()
+            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()).create();
+
+    @Test
+    void localDateTimeWhenInvalidShouldThrowJsonParseException() {
+        assertThrows(JsonParseException.class, () -> {
+            gson.fromJson("\"invalid\"", LocalDateTime.class);
+        });
+    }
+
+    @Test
+    void instantWhenValidShouldParse() {
+        assertThat((@Nullable LocalDateTime) gson.fromJson("\"2023-04-17T20:38:01\"", LocalDateTime.class),
+                is(equalTo(LocalDateTime.of(2023, 4, 17, 20, 38, 1, 0))));
+    }
+}
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 (file)
index 0000000..90a6c43
--- /dev/null
@@ -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 (file)
index 0000000..22f4b21
--- /dev/null
@@ -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 (file)
index 0000000..4d8b6ab
--- /dev/null
@@ -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 (file)
index 0000000..fce1f18
--- /dev/null
@@ -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 (file)
index 0000000..8d26514
--- /dev/null
@@ -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 (file)
index 0000000..7641d13
--- /dev/null
@@ -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 (file)
index 0000000..e0610a8
--- /dev/null
@@ -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 (file)
index 0000000..5ee45ea
--- /dev/null
@@ -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 (file)
index 0000000..1af9754
--- /dev/null
@@ -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 (file)
index 0000000..76423da
--- /dev/null
@@ -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 (file)
index 0000000..ce12925
--- /dev/null
@@ -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 (file)
index 0000000..8b1d42b
--- /dev/null
@@ -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 (file)
index 0000000..c2ed993
--- /dev/null
@@ -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
+               }
+       ]
+}
index cf42108c5e82bf67d359ed6ee3443a905be3a0bc..4a2b07e052c79bd6b2e1bf37e8f0f4adf2622635 100644 (file)
     <module>org.openhab.binding.elerotransmitterstick</module>
     <module>org.openhab.binding.elroconnects</module>
     <module>org.openhab.binding.energenie</module>
+    <module>org.openhab.binding.energidataservice</module>
     <module>org.openhab.binding.enigma2</module>
     <module>org.openhab.binding.enocean</module>
     <module>org.openhab.binding.enphase</module>