From df69f50d25c87bc86b6923b2776e2e764188f0fb Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 3 Jul 2023 18:16:17 +0200 Subject: [PATCH] [energidataservice] Initial contribution (#14376) * Initial contribution Signed-off-by: Jacob Laursen * Remove Value-Added Tax Signed-off-by: Jacob Laursen * Migrate naming convention Signed-off-by: Jacob Laursen * Add channel configuration example Signed-off-by: Jacob Laursen * Remove current prefixes for forward compatibility with timestamped items Signed-off-by: Jacob Laursen * Add filter for another grid company Signed-off-by: Jacob Laursen * Use ISO 3166-1 alpha-2 codes in lowercase for XSD compliance Signed-off-by: Jacob Laursen * Fix error handling for deserializers Signed-off-by: Jacob Laursen * Fix compliance with RFC 9110 section 10.1.5 Signed-off-by: Jacob Laursen * Add JavaScript example code Signed-off-by: Jacob Laursen * Refactor List to Collection and use iterators Signed-off-by: Jacob Laursen * Add filter for another grid company Signed-off-by: Jacob Laursen * Extend cached history to 24 hours Signed-off-by: Jacob Laursen * Remove filter for expired GLN Signed-off-by: Jacob Laursen * Fix typos Signed-off-by: Jacob Laursen * Improve descriptions Signed-off-by: Jacob Laursen * Improve logging Signed-off-by: Jacob Laursen --------- Signed-off-by: Jacob Laursen Signed-off-by: Matt Myers --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 13 + .../README.md | 472 +++ .../pom.xml | 25 + .../src/main/feature/feature.xml | 9 + .../internal/ApiController.java | 254 ++ .../internal/CacheManager.java | 443 +++ .../EnergiDataServiceBindingConstants.java | 79 + .../internal/PriceCalculator.java | 238 ++ .../internal/PriceListParser.java | 109 + .../action/EnergiDataServiceActions.java | 381 +++ .../internal/api/ChargeType.java | 39 + .../internal/api/ChargeTypeCode.java | 47 + .../internal/api/DatahubTariffFilter.java | 58 + .../api/DatahubTariffFilterFactory.java | 172 + .../internal/api/DateQueryParameter.java | 90 + .../internal/api/DateQueryParameterType.java | 40 + .../internal/api/GlobalLocationNumber.java | 73 + .../api/dto/DatahubPricelistRecord.java | 202 ++ .../api/dto/DatahubPricelistRecords.java | 25 + .../internal/api/dto/ElspotpriceRecord.java | 31 + .../internal/api/dto/ElspotpriceRecords.java | 24 + .../serialization/InstantDeserializer.java | 47 + .../serialization/LocalDateDeserializer.java | 44 + .../LocalDateTimeDeserializer.java | 44 + .../config/DatahubPriceConfiguration.java | 103 + .../EnergiDataServiceConfiguration.java | 65 + .../exception/DataServiceException.java | 50 + .../exception/MissingPriceException.java | 31 + .../EnergiDataServiceHandlerFactory.java | 73 + .../handler/EnergiDataServiceHandler.java | 541 +++ .../internal/retry/RetryPolicyFactory.java | 89 + .../internal/retry/RetryStrategy.java | 34 + .../retry/strategy/ExponentialBackoff.java | 105 + .../internal/retry/strategy/FixedTime.java | 86 + .../internal/retry/strategy/Linear.java | 96 + .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../main/resources/OH-INF/config/config.xml | 94 + .../OH-INF/i18n/energidataservice.properties | 118 + .../resources/OH-INF/thing/channel-groups.xml | 35 + .../resources/OH-INF/thing/channel-types.xml | 32 + .../resources/OH-INF/thing/thing-service.xml | 19 + .../internal/CacheManagerTest.java | 126 + .../internal/PriceListParserTest.java | 194 ++ .../action/EnergiDataServiceActionsTest.java | 403 +++ .../internal/api/DateQueryParameterTest.java | 65 + .../api/GlobalLocationNumberTest.java | 46 + .../InstantDeserializerTest.java | 58 + .../LocalDateDeserializerTest.java | 56 + .../strategy/ExponentialBackoffTest.java | 39 + .../retry/strategy/FixedTimeTest.java | 57 + .../internal/retry/strategy/LinearTest.java | 39 + .../DatahubPricelistElectricityTax.json | 83 + .../internal/DatahubPricelistN1.json | 588 ++++ .../internal/DatahubPricelistNordEnergi.json | 37 + .../internal/DatahubPricelistTrefor.json | 2908 +++++++++++++++++ .../internal/action/ElectricityTaxes.json | 45 + .../internal/action/NetTariffs.json | 37 + .../internal/action/SpotPrices20230204.json | 142 + .../internal/action/SpotPrices20230205.json | 142 + .../internal/action/SystemTariffs.json | 36 + .../action/TransmissionNetTariffs.json | 36 + bundles/pom.xml | 1 + 64 files changed, 9685 insertions(+) create mode 100644 bundles/org.openhab.binding.energidataservice/NOTICE create mode 100644 bundles/org.openhab.binding.energidataservice/README.md create mode 100644 bundles/org.openhab.binding.energidataservice/pom.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializerTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializerTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json create mode 100644 bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json diff --git a/CODEOWNERS b/CODEOWNERS index c737e7c7ef09d..a6d6e160d3858 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 54161aed4e1c2..2a48a1987d3a1 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -456,6 +456,11 @@ org.openhab.binding.energenie ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.energidataservice + ${project.version} + org.openhab.addons.bundles org.openhab.binding.enigma2 diff --git a/bundles/org.openhab.binding.energidataservice/NOTICE b/bundles/org.openhab.binding.energidataservice/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md new file mode 100644 index 0000000000000..41bcbe7dc251e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -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 result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90)) +``` + +#### `calculateCheapestPeriod` from Duration and Power + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| duration | `Duration` | The duration to fit within the timeslot | +| power | `QuantityType` | Linear power consumption | + +This action is identical 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 result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90), 250 | W) +``` + +#### `calculateCheapestPeriod` from Power Phases + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| durationPhases | `List` | List of durations for the phases | +| powerPhases | `List>` | List of power consumption for each corresponding phase | + +This variant is similar to the one above, but is based on a supplied timetable. + +The timetable is supplied as two individual parameters, `durationPhases` and `powerPhases`, which must have the same size. +This can be considered as different phases of using power, so each list member represents a period with a linear use of power. +`durationPhases` should be a List populated by `Duration` objects, while `powerPhases` should be a List populated by `QuantityType` objects for that duration of time. + +Example: + +```javascript +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) +durationPhases.add(Duration.ofMinutes(104)) + +val ArrayList> powerPhases = new ArrayList>() +powerPhases.add(162.162 | W) +powerPhases.add(750 | W) +powerPhases.add(1500 | W) +powerPhases.add(3000 | W) +powerPhases.add(1500 | W) +powerPhases.add(166.666 | W) +powerPhases.add(146.341 | W) +powerPhases.add(0 | W) + +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), durationPhases, powerPhases) +``` + +Please note that the total duration will be calculated automatically as a sum of provided duration phases. +Therefore, if the total duration is longer than the sum of phase durations, the remaining duration must be provided as last item with a corresponding 0 W power item. +This is to ensure that the full program will finish before the provided `latestEnd`. + +#### `calculateCheapestPeriod` from Energy per Phase + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| totalDuration | `Duration` | The total duration of all phases | +| durationPhases | `List` | List of durations for the phases | +| energyUsedPerPhase | `QuantityType` | Fixed amount of energy used per phase | + +This variant will assign the provided amount of energy into each phase. +The use case for this variant is a simplification of the previous variant. +For example, a dishwasher may provide energy consumption in 0.1 kWh steps. +In this case it's a simple task to create a timetable accordingly without having to calculate the average power consumption per phase. +Since a last phase may use no significant energy, the total duration must be provided also. + +Example: + +```javascript +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) + +// 0.7 kWh is used in total (number of phases × energy used per phase) +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh) +``` + +### `calculatePrice` + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| start | `Instant` | Start time | +| end | `Instant` | End time | +| power | `QuantityType` | Linear power consumption | + +**Result:** Price as `BigDecimal`. + +This action calculates the price for using given amount of power in the period from `start` till `end`. + +Example: + +```javascript +var price = actions.calculatePrice(now.toInstant(), now.plusHours(4).toInstant, 200 | W) +``` + +### `getPrices` + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| priceElements | `String` | Comma-separated list of price elements to include | + +**Result:** `Map` + +The parameter `priceElements` is a case-insensitive comma-separated list of price elements to include in the returned hourly prices. +These elements can be requested: + +| Price element | Description | +|-----------------------|-------------------------| +| SpotPrice | Spot price | +| NetTariff | Net tariff | +| SystemTariff | System tariff | +| ElectricityTax | Electricity tax | +| TransmissionNetTariff | Transmission net tariff | + +Using `null` as parameter returns the total prices including all price elements. + +Example: + +```javascript +var priceMap = actions.getPrices("SpotPrice,NetTariff"); +``` + +## Full Example + +### Thing Configuration + +```java +Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] { + Channels: + Number : electricity#net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ] +} +``` + +### Item Configuration + +```java +Group:Number:SUM TotalPrice "Current Total Price" +Number SpotPrice "Current Spot Price" (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#spot-price" [profile="transform:VAT"] } +Number NetTariff "Current Net Tariff" (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#net-tariff" [profile="transform:VAT"] } +Number SystemTariff "Current System Tariff" (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#system-tariff" [profile="transform:VAT"] } +Number ElectricityTax "Current Electricity Tax" (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#electricity-tax" [profile="transform:VAT"] } +Number TransmissionNetTariff "Current Transmission Tariff" (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#transmission-net-tariff" [profile="transform:VAT"] } +String HourlyPrices "Hourly Prices" { 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 durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) +durationPhases.add(Duration.ofMinutes(104)) + +val ArrayList> consumptionPhases = new ArrayList>() +consumptionPhases.add(162.162 | W) +consumptionPhases.add(750 | W) +consumptionPhases.add(1500 | W) +consumptionPhases.add(3000 | W) +consumptionPhases.add(1500 | W) +consumptionPhases.add(166.666 | W) +consumptionPhases.add(146.341 | W) +consumptionPhases.add(0 | W) + +var Map result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases) +logInfo("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 durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) + +var Map 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 index 0000000000000..4e4d39fddfb21 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.energidataservice + + openHAB Add-ons :: Bundles :: Energi Data Service Binding + + + + com.google.code.gson + gson + 2.10.1 + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml new file mode 100644 index 0000000000000..69ed1af643ffc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.energidataservice/${project.version} + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java new file mode 100644 index 0000000000000..484986ff819d5 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Currency; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.energidataservice.internal.api.ChargeType; +import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; +import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords; +import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer; +import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; +import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.core.i18n.TimeZoneProvider; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link ApiController} is responsible for interacting with Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ApiController { + private static final String ENDPOINT = "https://api.energidataservice.dk/"; + private static final String DATASET_PATH = "dataset/"; + + private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices"; + private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist"; + + private static final String FILTER_KEY_PRICE_AREA = "PriceArea"; + private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType"; + private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode"; + private static final String FILTER_KEY_GLN_NUMBER = "GLN_Number"; + private static final String FILTER_KEY_NOTE = "Note"; + + private static final String HEADER_REMAINING_CALLS = "RemainingCalls"; + private static final String HEADER_TOTAL_CALLS = "TotalCalls"; + + private final Logger logger = LoggerFactory.getLogger(ApiController.class); + private final Gson gson = new GsonBuilder() // + .registerTypeAdapter(Instant.class, new InstantDeserializer()) // + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) // + .create(); + private final HttpClient httpClient; + private final TimeZoneProvider timeZoneProvider; + private final String userAgent; + + public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) { + this.httpClient = httpClient; + this.timeZoneProvider = timeZoneProvider; + userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); + } + + /** + * Retrieve spot prices for requested area and in requested {@link Currency}. + * + * @param priceArea Usually DK1 or DK2 + * @param currency DKK or EUR + * @param start Specifies the start point of the period for the data request + * @param properties Map of properties which will be updated with metadata from headers + * @return Records with pairs of hour start and price in requested currency. + * @throws InterruptedException + * @throws DataServiceException + */ + public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start, + Map properties) throws InterruptedException, DataServiceException { + if (!SUPPORTED_CURRENCIES.contains(currency)) { + throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); + } + + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES) + .param("start", start.toString()) // + .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") // + .param("columns", "HourUTC,SpotPrice" + currency) // + .agent(userAgent) // + .method(HttpMethod.GET); + + logger.trace("GET request for {}", request.getURI()); + + try { + ContentResponse response = request.send(); + + updatePropertiesFromResponse(response, properties); + + int status = response.getStatus(); + if (!HttpStatus.isSuccess(status)) { + throw new DataServiceException("The request failed with HTTP error " + status, status); + } + String responseContent = response.getContentAsString(); + if (responseContent.isEmpty()) { + throw new DataServiceException("Empty response"); + } + logger.trace("Response content: '{}'", responseContent); + + ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); + if (records == null) { + throw new DataServiceException("Error parsing response"); + } + + if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) { + throw new DataServiceException("No records"); + } + + return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(ElspotpriceRecord[]::new); + } catch (JsonSyntaxException e) { + throw new DataServiceException("Error parsing response", e); + } catch (TimeoutException | ExecutionException e) { + throw new DataServiceException(e); + } + } + + private void updatePropertiesFromResponse(ContentResponse response, Map properties) { + HttpFields headers = response.getHeaders(); + String remainingCalls = headers.get(HEADER_REMAINING_CALLS); + if (remainingCalls != null) { + properties.put(PROPERTY_REMAINING_CALLS, remainingCalls); + } + String totalCalls = headers.get(HEADER_TOTAL_CALLS); + if (totalCalls != null) { + properties.put(PROPERTY_TOTAL_CALLS, totalCalls); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT); + properties.put(PROPERTY_LAST_CALL, LocalDateTime.now(timeZoneProvider.getTimeZone()).format(formatter)); + } + + /** + * Retrieve datahub pricelists for requested GLN and charge type/charge type code. + * + * @param globalLocationNumber Global Location Number of the Charge Owner + * @param chargeType Charge type (Subscription, Fee or Tariff). + * @param tariffFilter Tariff filter (charge type codes and notes). + * @param properties Map of properties which will be updated with metadata from headers + * @return Price list for requested GLN and note. + * @throws InterruptedException + * @throws DataServiceException + */ + public Collection getDatahubPriceLists(GlobalLocationNumber globalLocationNumber, + ChargeType chargeType, DatahubTariffFilter tariffFilter, Map properties) + throws InterruptedException, DataServiceException { + String columns = "ValidFrom,ValidTo,ChargeTypeCode"; + for (int i = 1; i < 25; i++) { + columns += ",Price" + i; + } + + Map> filterMap = new HashMap<>(Map.of( // + FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), // + FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString()))); + + Collection chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings(); + if (!chargeTypeCodes.isEmpty()) { + filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes); + } + + Collection notes = tariffFilter.getNotes(); + if (!notes.isEmpty()) { + filterMap.put(FILTER_KEY_NOTE, notes); + } + + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST) + .param("filter", mapToFilter(filterMap)) // + .param("columns", columns) // + .agent(userAgent) // + .method(HttpMethod.GET); + + DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter(); + if (!dateQueryParameter.isEmpty()) { + request = request.param("start", dateQueryParameter.toString()); + } + + logger.trace("GET request for {}", request.getURI()); + + try { + ContentResponse response = request.send(); + + updatePropertiesFromResponse(response, properties); + + int status = response.getStatus(); + if (!HttpStatus.isSuccess(status)) { + throw new DataServiceException("The request failed with HTTP error " + status, status); + } + String responseContent = response.getContentAsString(); + if (responseContent.isEmpty()) { + throw new DataServiceException("Empty response"); + } + logger.trace("Response content: '{}'", responseContent); + + DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class); + if (records == null) { + throw new DataServiceException("Error parsing response"); + } + + if (records.limit() > 0 && records.limit() < records.total()) { + logger.warn("{} price list records available, but only {} returned.", records.total(), records.limit()); + } + + if (Objects.isNull(records.records())) { + return List.of(); + } + + return Arrays.stream(records.records()).filter(Objects::nonNull).toList(); + } catch (JsonSyntaxException e) { + throw new DataServiceException("Error parsing response", e); + } catch (TimeoutException | ExecutionException e) { + throw new DataServiceException(e); + } + } + + private String mapToFilter(Map> map) { + return "{" + map.entrySet().stream().map( + e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]") + .collect(Collectors.joining(",")) + "}"; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java new file mode 100644 index 0000000000000..d73ee12dd1d91 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; + +/** + * The {@link CacheManager} is responsible for maintaining a cache of received + * data from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class CacheManager { + + public 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 netTariffRecords = new ArrayList<>(); + private Collection systemTariffRecords = new ArrayList<>(); + private Collection electricityTaxRecords = new ArrayList<>(); + private Collection transmissionNetTariffRecords = new ArrayList<>(); + + private Map spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE); + private Map netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map transmissionNetTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + + public CacheManager() { + this(Clock.systemDefaultZone()); + } + + public CacheManager(Clock clock) { + this.clock = clock.withZone(NORD_POOL_TIMEZONE); + } + + /** + * Clear all cached data. + */ + public void clear() { + netTariffRecords.clear(); + systemTariffRecords.clear(); + electricityTaxRecords.clear(); + transmissionNetTariffRecords.clear(); + + spotPriceMap.clear(); + netTariffMap.clear(); + systemTariffMap.clear(); + electricityTaxMap.clear(); + transmissionNetTariffMap.clear(); + } + + /** + * Convert and cache the supplied {@link ElspotpriceRecord}s. + * + * @param records The records as received from Energi Data Service. + * @param currency The currency in which the records were requested. + */ + public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) { + boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency); + for (ElspotpriceRecord record : records) { + spotPriceMap.put(record.hour(), + (isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000))); + } + cleanup(); + } + + /** + * Replace current "raw"/unprocessed net tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putNetTariffs(Collection records) { + putDatahubRecords(netTariffRecords, records); + updateNetTariffs(); + } + + /** + * Replace current "raw"/unprocessed system tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putSystemTariffs(Collection records) { + putDatahubRecords(systemTariffRecords, records); + updateSystemTariffs(); + } + + /** + * Replace current "raw"/unprocessed electricity tax records in cache. + * Map of hourly taxes will be updated automatically. + * + * @param records to cache + */ + public void putElectricityTaxes(Collection records) { + putDatahubRecords(electricityTaxRecords, records); + updateElectricityTaxes(); + } + + /** + * Replace current "raw"/unprocessed transmission net tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putTransmissionNetTariffs(Collection records) { + putDatahubRecords(transmissionNetTariffRecords, records); + updateTransmissionNetTariffs(); + } + + private void putDatahubRecords(Collection destination, + Collection source) { + LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE)) + .minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS); + + destination.clear(); + destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList()); + } + + /** + * Update map of hourly net tariffs from internal cache. + */ + public void updateNetTariffs() { + netTariffMap = priceListParser.toHourly(netTariffRecords); + cleanup(); + } + + /** + * Update map of system tariffs from internal cache. + */ + public void updateSystemTariffs() { + systemTariffMap = priceListParser.toHourly(systemTariffRecords); + cleanup(); + } + + /** + * Update map of electricity taxes from internal cache. + */ + public void updateElectricityTaxes() { + electricityTaxMap = priceListParser.toHourly(electricityTaxRecords); + cleanup(); + } + + /** + * Update map of hourly transmission net tariffs from internal cache. + */ + public void updateTransmissionNetTariffs() { + transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords); + cleanup(); + } + + /** + * Get current spot price. + * + * @return spot price currently valid + */ + public @Nullable BigDecimal getSpotPrice() { + return getSpotPrice(Instant.now(clock)); + } + + /** + * Get spot price valid at provided instant. + * + * @param time {@link Instant} for which to get the spot price + * @return spot price at given time or null if not available + */ + public @Nullable BigDecimal getSpotPrice(Instant time) { + return spotPriceMap.get(getHourStart(time)); + } + + /** + * Get map of all cached spot prices. + * + * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getSpotPrices() { + return new HashMap(spotPriceMap); + } + + /** + * Get current net tariff. + * + * @return net tariff currently valid + */ + public @Nullable BigDecimal getNetTariff() { + return getNetTariff(Instant.now(clock)); + } + + /** + * Get net tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the net tariff + * @return net tariff at given time or null if not available + */ + public @Nullable BigDecimal getNetTariff(Instant time) { + return netTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached net tariffs. + * + * @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getNetTariffs() { + return new HashMap(netTariffMap); + } + + /** + * Get current system tariff. + * + * @return system tariff currently valid + */ + public @Nullable BigDecimal getSystemTariff() { + return getSystemTariff(Instant.now(clock)); + } + + /** + * Get system tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the system tariff + * @return system tariff at given time or null if not available + */ + public @Nullable BigDecimal getSystemTariff(Instant time) { + return systemTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached system tariffs. + * + * @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getSystemTariffs() { + return new HashMap(systemTariffMap); + } + + /** + * Get current electricity tax. + * + * @return electricity tax currently valid + */ + public @Nullable BigDecimal getElectricityTax() { + return getElectricityTax(Instant.now(clock)); + } + + /** + * Get electricity tax valid at provided instant. + * + * @param time {@link Instant} for which to get the electricity tax + * @return electricity tax at given time or null if not available + */ + public @Nullable BigDecimal getElectricityTax(Instant time) { + return electricityTaxMap.get(getHourStart(time)); + } + + /** + * Get map of all cached electricity taxes. + * + * @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getElectricityTaxes() { + return new HashMap(electricityTaxMap); + } + + /** + * Get current transmission net tariff. + * + * @return transmission net tariff currently valid + */ + public @Nullable BigDecimal getTransmissionNetTariff() { + return getTransmissionNetTariff(Instant.now(clock)); + } + + /** + * Get transmission net tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the transmission net tariff + * @return transmission net tariff at given time or null if not available + */ + public @Nullable BigDecimal getTransmissionNetTariff(Instant time) { + return transmissionNetTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached transmission net tariffs. + * + * @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getTransmissionNetTariffs() { + return new HashMap(transmissionNetTariffMap); + } + + /** + * Get number of future spot prices including current hour. + * + * @return number of future spot prices + */ + public long getNumberOfFutureSpotPrices() { + Instant currentHourStart = getCurrentHourStart(); + + return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count(); + } + + /** + * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached. + * + * @return true if historic spot prices are cached + */ + public boolean areHistoricSpotPricesCached() { + return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS)); + } + + /** + * Check if all current spot prices are cached taking into consideration that next day's spot prices + * should be available at 13:00 CET. + * + * @return true if spot prices are fully cached + */ + public boolean areSpotPricesFullyCached() { + Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant(); + LocalTime now = LocalTime.now(clock); + if (now.isAfter(DAILY_REFRESH_TIME_CET)) { + end = end.plus(24, ChronoUnit.HOURS); + } + + return arePricesCached(spotPriceMap, end); + } + + private boolean arePricesCached(Map priceMap, Instant end) { + for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1, + ChronoUnit.HOURS)) { + if (priceMap.get(hourStart) == null) { + return false; + } + } + + return true; + } + + /** + * Check if we have "raw" net tariff records cached which are valid tomorrow. + * + * @return true if net tariff records for tomorrow are cached + */ + public boolean areNetTariffsValidTomorrow() { + return isValidNextDay(netTariffRecords); + } + + /** + * Check if we have "raw" system tariff records cached which are valid tomorrow. + * + * @return true if system tariff records for tomorrow are cached + */ + public boolean areSystemTariffsValidTomorrow() { + return isValidNextDay(systemTariffRecords); + } + + /** + * Check if we have "raw" electricity tax records cached which are valid tomorrow. + * + * @return true if electricity tax records for tomorrow are cached + */ + public boolean areElectricityTaxesValidTomorrow() { + return isValidNextDay(electricityTaxRecords); + } + + /** + * Check if we have "raw" transmission net tariff records cached which are valid tomorrow. + * + * @return true if transmission net tariff records for tomorrow are cached + */ + public boolean areTransmissionNetTariffsValidTomorrow() { + return isValidNextDay(transmissionNetTariffRecords); + } + + /** + * Remove historic prices. + */ + public void cleanup() { + Instant firstHourStart = getFirstHourStart(); + + spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + netTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + systemTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + electricityTaxMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + transmissionNetTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + } + + private boolean isValidNextDay(Collection records) { + LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE) + .truncatedTo(ChronoUnit.HOURS); + LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS); + + return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight)); + } + + private Instant getCurrentHourStart() { + return getHourStart(Instant.now(clock)); + } + + private Instant getFirstHourStart() { + return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)); + } + + private Instant getHourStart(Instant instant) { + return instant.truncatedTo(ChronoUnit.HOURS); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java new file mode 100644 index 0000000000000..aa828627a98f0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java @@ -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 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 SUPPORTED_CURRENCIES = Set.of(CURRENCY_DKK, CURRENCY_EUR); + + // Time-zone of Datahub + public static final ZoneId DATAHUB_TIMEZONE = ZoneId.of("CET"); + public static final ZoneId NORD_POOL_TIMEZONE = ZoneId.of("CET"); + + // Other + public static final LocalTime DAILY_REFRESH_TIME_CET = LocalTime.of(13, 0); + public static final LocalDate ENERGINET_CUTOFF_DATE = LocalDate.of(2023, 1, 1); + public static final String PROPERTY_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java new file mode 100644 index 0000000000000..67e988909bc2f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java @@ -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 priceMap; + + public PriceCalculator(Map priceMap) { + this.priceMap = priceMap; + } + + /** + * Calculate cheapest period from list of durations with specified amount of energy + * used per phase. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param totalDuration Total duration to fit. + * @param durationPhases List of {@link Duration}'s representing different phases of using power. + * @param energyUsedPerPhase Amount of energy used per phase. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration, + Collection durationPhases, QuantityType energyUsedPerPhase) throws MissingPriceException { + QuantityType energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR); + if (energyInWattHour == null) { + throw new IllegalArgumentException( + "Invalid unit " + energyUsedPerPhase.getUnit() + ", expected energy unit"); + } + // watts = (kWh × 1,000) ÷ hrs + int numerator = energyInWattHour.intValue() * 3600; + List> consumptionPhases = new ArrayList<>(); + Duration remainingDuration = totalDuration; + for (Duration phase : durationPhases) { + consumptionPhases.add(QuantityType.valueOf(numerator / phase.getSeconds(), Units.WATT)); + remainingDuration = remainingDuration.minus(phase); + } + if (remainingDuration.isNegative()) { + throw new IllegalArgumentException("totalDuration must be equal to or greater than sum of phases"); + } + if (!remainingDuration.isZero()) { + List durationsWithTermination = new ArrayList<>(durationPhases); + durationsWithTermination.add(remainingDuration); + consumptionPhases.add(QuantityType.valueOf(0, Units.WATT)); + return calculateCheapestPeriod(earliestStart, latestEnd, durationsWithTermination, consumptionPhases); + } + return calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, consumptionPhases); + } + + /** + * Calculate cheapest period from duration with linear power usage. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param duration Duration to fit. + * @param power Power consumption for the duration of time. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration, + QuantityType power) throws MissingPriceException { + return calculateCheapestPeriod(earliestStart, latestEnd, List.of(duration), List.of(power)); + } + + /** + * Calculate cheapest period from list of durations with corresponding list of consumption + * per duration. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param durationPhases List of {@link Duration}'s representing different phases of using power. + * @param consumptionPhases Corresponding List of power consumption for the duration of time. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, + Collection durationPhases, Collection> consumptionPhases) + throws MissingPriceException { + if (durationPhases.size() != consumptionPhases.size()) { + throw new IllegalArgumentException("Number of phases do not match"); + } + Map result = new HashMap<>(); + Duration totalDuration = durationPhases.stream().reduce(Duration.ZERO, Duration::plus); + Instant calculationStart = earliestStart; + Instant calculationEnd = earliestStart.plus(totalDuration); + BigDecimal lowestPrice = BigDecimal.valueOf(Double.MAX_VALUE); + BigDecimal highestPrice = BigDecimal.ZERO; + Instant cheapestStart = Instant.MIN; + Instant mostExpensiveStart = Instant.MIN; + + while (calculationEnd.compareTo(latestEnd) <= 0) { + BigDecimal currentPrice = BigDecimal.ZERO; + Duration minDurationUntilNextHour = Duration.ofHours(1); + Instant atomStart = calculationStart; + + Iterator durationIterator = durationPhases.iterator(); + Iterator> consumptionIterator = consumptionPhases.iterator(); + while (durationIterator.hasNext()) { + Duration atomDuration = durationIterator.next(); + QuantityType 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) + throws MissingPriceException { + QuantityType quantityInWatt = power.toUnit(Units.WATT); + if (quantityInWatt == null) { + throw new IllegalArgumentException("Invalid unit " + power.getUnit() + ", expected power unit"); + } + BigDecimal watt = new BigDecimal(quantityInWatt.intValue()); + if (watt.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + + Instant current = start; + BigDecimal result = BigDecimal.ZERO; + while (current.isBefore(end)) { + Instant hourStart = current.truncatedTo(ChronoUnit.HOURS); + Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS); + + BigDecimal currentPrice = priceMap.get(hourStart); + if (currentPrice == null) { + throw new MissingPriceException("Price missing at " + hourStart.toString()); + } + + Instant currentStart = hourStart; + if (start.isAfter(hourStart)) { + currentStart = start; + } + Instant currentEnd = hourEnd; + if (end.isBefore(hourEnd)) { + currentEnd = end; + } + + // E(kWh) = P(W) × t(hr) / 1000 + Duration duration = Duration.between(currentStart, currentEnd); + BigDecimal contribution = currentPrice.multiply(watt).multiply( + new BigDecimal(duration.getSeconds()).divide(new BigDecimal(3600000), 9, RoundingMode.HALF_UP)); + result = result.add(contribution); + logger.trace("Period {}-{}: {} @ {}", currentStart, currentEnd, contribution, currentPrice); + + current = hourEnd; + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java new file mode 100644 index 0000000000000..5f4bedc1a3f31 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; + +/** + * Parses results from {@link DatahubPricelistRecords} into map of hourly tariffs. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class PriceListParser { + + private final Clock clock; + + public PriceListParser() { + this(Clock.system(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + } + + public PriceListParser(Clock clock) { + this.clock = clock; + } + + public Map toHourly(Collection records) { + Map totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); + records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> { + Map currentMap = toHourly(records, chargeTypeCode); + for (Entry current : currentMap.entrySet()) { + BigDecimal total = totalMap.get(current.getKey()); + if (total == null) { + total = BigDecimal.ZERO; + } + totalMap.put(current.getKey(), total.add(current.getValue())); + } + }); + + return totalMap; + } + + public Map toHourly(Collection records, String chargeTypeCode) { + Map tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); + + Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS) + .truncatedTo(ChronoUnit.HOURS); + Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS) + .truncatedTo(ChronoUnit.DAYS); + + LocalDateTime previousValidFrom = LocalDateTime.MAX; + LocalDateTime previousValidTo = LocalDateTime.MIN; + Map tariffs = Map.of(); + for (Instant hourStart = firstHourStart; hourStart + .isBefore(lastHourStart); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) { + LocalDateTime localDateTime = hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE) + .toLocalDateTime(); + if (localDateTime.compareTo(previousValidFrom) < 0 || localDateTime.compareTo(previousValidTo) >= 0) { + DatahubPricelistRecord priceList = getTariffs(records, localDateTime, chargeTypeCode); + if (priceList != null) { + tariffs = priceList.getTariffMap(); + previousValidFrom = priceList.validFrom(); + previousValidTo = priceList.validTo(); + } else { + tariffs = Map.of(); + } + } + + LocalTime localTime = LocalTime + .of(hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE).getHour(), 0); + BigDecimal tariff = tariffs.get(localTime); + if (tariff != null) { + tariffMap.put(hourStart, tariff); + } + } + + return tariffMap; + } + + private @Nullable DatahubPricelistRecord getTariffs(Collection records, + LocalDateTime localDateTime, String chargeTypeCode) { + return records.stream() + .filter(record -> localDateTime.compareTo(record.validFrom()) >= 0 + && localDateTime.compareTo(record.validTo()) < 0 + && record.chargeTypeCode().equals(chargeTypeCode)) + .findFirst().orElse(null); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java new file mode 100644 index 0000000000000..a39897deb7137 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java @@ -0,0 +1,381 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.action; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.PriceCalculator; +import org.openhab.binding.energidataservice.internal.exception.MissingPriceException; +import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.ActionOutput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context. + * + * @author Jacob Laursen - Initial contribution + */ +@ThingActionsScope(name = "energidataservice") +@NonNullByDefault +public class EnergiDataServiceActions implements ThingActions { + + private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class); + + private @Nullable EnergiDataServiceHandler handler; + + private enum PriceElement { + SPOT_PRICE("spotprice"), + NET_TARIFF("nettariff"), + SYSTEM_TARIFF("systemtariff"), + ELECTRICITY_TAX("electricitytax"), + TRANSMISSION_NET_TARIFF("transmissionnettariff"); + + private static final Map NAME_MAP = Stream.of(values()) + .collect(Collectors.toMap(PriceElement::toString, Function.identity())); + + private String name; + + private PriceElement(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static PriceElement fromString(final String name) { + PriceElement myEnum = NAME_MAP.get(name.toLowerCase()); + if (null == myEnum) { + throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s", + name, Arrays.asList(values()))); + } + return myEnum; + } + } + + @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description") + public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices() { + return getPrices(Arrays.stream(PriceElement.values()).collect(Collectors.toSet())); + } + + @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description") + public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices( + @ActionInput(name = "priceElements", label = "@text/action.get-prices.priceElements.label", description = "@text/action.get-prices.priceElements.description") @Nullable String priceElements) { + if (priceElements == null) { + logger.warn("Argument 'priceElements' is null"); + return Map.of(); + } + + Set priceElementsSet; + try { + priceElementsSet = new HashSet( + Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList()); + } catch (IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + + return getPrices(priceElementsSet); + } + + @RuleAction(label = "@text/action.calculate-price.label", description = "@text/action.calculate-price.description") + public @ActionOutput(name = "price", type = "java.math.BigDecimal") BigDecimal calculatePrice( + @ActionInput(name = "start", type = "java.time.Instant") Instant start, + @ActionInput(name = "end", type = "java.time.Instant") Instant end, + @ActionInput(name = "power", type = "QuantityType") QuantityType power) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculatePrice(start, end, power); + } catch (MissingPriceException e) { + logger.warn("{}", e.getMessage()); + return BigDecimal.ZERO; + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "duration", type = "java.time.Duration") Duration duration) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + Map intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, + duration, QuantityType.valueOf(1000, Units.WATT)); + + // Create new result with stripped price information. + Map result = new HashMap<>(); + Object value = intermediateResult.get("CheapestStart"); + if (value != null) { + result.put("CheapestStart", value); + } + value = intermediateResult.get("MostExpensiveStart"); + if (value != null) { + result.put("MostExpensiveStart", value); + } + return result; + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "duration", type = "java.time.Duration") Duration duration, + @ActionInput(name = "power", type = "QuantityType") QuantityType power) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "totalDuration", type = "java.time.Duration") Duration totalDuration, + @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases, + @ActionInput(name = "energyUsedPerPhase", type = "QuantityType") QuantityType energyUsedPerPhase) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases, + energyUsedPerPhase); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases, + @ActionInput(name = "powerPhases", type = "java.util.List>") List> powerPhases) { + if (durationPhases.size() != powerPhases.size()) { + logger.warn("Number of duration phases ({}) is different from number of consumption phases ({})", + durationPhases.size(), powerPhases.size()); + return Map.of(); + } + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + private Map getPrices(Set priceElements) { + EnergiDataServiceHandler handler = this.handler; + if (handler == null) { + logger.warn("EnergiDataServiceActions ThingHandler is null."); + return Map.of(); + } + + Map prices; + boolean spotPricesRequired; + if (priceElements.contains(PriceElement.SPOT_PRICE)) { + if (priceElements.size() > 1 && !handler.getCurrency().equals(CURRENCY_DKK)) { + logger.warn("Cannot calculate sum when spot price currency is {}", handler.getCurrency()); + return Map.of(); + } + prices = handler.getSpotPrices(); + spotPricesRequired = true; + } else { + spotPricesRequired = false; + prices = new HashMap<>(); + } + + if (priceElements.contains(PriceElement.NET_TARIFF)) { + Map netTariffMap = handler.getNetTariffs(); + mergeMaps(prices, netTariffMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) { + Map systemTariffMap = handler.getSystemTariffs(); + mergeMaps(prices, systemTariffMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) { + Map electricityTaxMap = handler.getElectricityTaxes(); + mergeMaps(prices, electricityTaxMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) { + Map transmissionNetTariffMap = handler.getTransmissionNetTariffs(); + mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired); + } + + return prices; + } + + private void mergeMaps(Map destinationMap, Map sourceMap, + boolean createNew) { + for (Entry source : sourceMap.entrySet()) { + Instant key = source.getKey(); + BigDecimal sourceValue = source.getValue(); + BigDecimal destinationValue = destinationMap.get(key); + if (destinationValue != null) { + destinationMap.put(key, sourceValue.add(destinationValue)); + } else if (createNew) { + destinationMap.put(key, sourceValue); + } + } + } + + /** + * Static get prices method for DSL rule compatibility. + * + * @param actions + * @param priceElements Comma-separated list of price elements to include in prices. + * @return Map of prices + */ + public static Map getPrices(@Nullable ThingActions actions, @Nullable String priceElements) { + if (actions instanceof EnergiDataServiceActions) { + if (priceElements != null && !priceElements.isBlank()) { + return ((EnergiDataServiceActions) actions).getPrices(priceElements); + } else { + return ((EnergiDataServiceActions) actions).getPrices(); + } + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + /** + * Static get prices method for DSL rule compatibility. + * + * @param actions + * @param start Start time + * @param end End time + * @param power Constant power consumption + * @return Map of prices + */ + public static BigDecimal calculatePrice(@Nullable ThingActions actions, @Nullable Instant start, + @Nullable Instant end, @Nullable QuantityType power) { + if (start == null || end == null || power == null) { + return BigDecimal.ZERO; + } + if (actions instanceof EnergiDataServiceActions) { + return ((EnergiDataServiceActions) actions).calculatePrice(start, end, power); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || duration == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration, + @Nullable QuantityType power) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || duration == null || power == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration, + power); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration, + @Nullable List durationPhases, @Nullable QuantityType energyUsedPerPhase) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || totalDuration == null || durationPhases == null + || energyUsedPerPhase == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, + durationPhases, energyUsedPerPhase); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List durationPhases, + @Nullable List> powerPhases) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || durationPhases == null || powerPhases == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, + durationPhases, powerPhases); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof EnergiDataServiceHandler) { + this.handler = (EnergiDataServiceHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java new file mode 100644 index 0000000000000..19d242b740915 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Charge type for DatahubPricelist dataset. + * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}} + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum ChargeType { + Subscription("D01"), + Fee("D02"), + Tariff("D03"); + + private final String code; + + ChargeType(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java new file mode 100644 index 0000000000000..b9228a17215bc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Charge type code for DatahubPricelist dataset. + * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}} + * These codes are defined by the individual grid companies. + * For example, N1 uses "CD" for "Nettarif C" and "CD R" for "Rabat på nettarif N1 A/S". + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ChargeTypeCode { + + private static final int MAX_LENGTH = 20; + + private final String code; + + public ChargeTypeCode(String code) { + if (code.length() > MAX_LENGTH) { + throw new IllegalArgumentException("Maximum length exceeded: " + code); + } + this.code = code; + } + + @Override + public String toString() { + return code; + } + + public static ChargeTypeCode of(String code) { + return new ChargeTypeCode(code); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java new file mode 100644 index 0000000000000..ecbec4a8bb94c --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Filter for the {@link DatahubPricelist} dataset. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DatahubTariffFilter { + + private final Set chargeTypeCodes; + private final Set notes; + private final DateQueryParameter dateQueryParameter; + + public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) { + this(filter.chargeTypeCodes, filter.notes, dateQueryParameter); + } + + public DatahubTariffFilter(Set chargeTypeCodes, Set notes) { + this(chargeTypeCodes, notes, DateQueryParameter.EMPTY); + } + + public DatahubTariffFilter(Set chargeTypeCodes, Set notes, + DateQueryParameter dateQueryParameter) { + this.chargeTypeCodes = chargeTypeCodes; + this.notes = notes; + this.dateQueryParameter = dateQueryParameter; + } + + public Collection getChargeTypeCodesAsStrings() { + return chargeTypeCodes.stream().map(c -> c.toString()).toList(); + } + + public Collection getNotes() { + return notes; + } + + public DateQueryParameter getDateQueryParameter() { + return dateQueryParameter; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java new file mode 100644 index 0000000000000..2809c1592a585 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java @@ -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 index 0000000000000..70f996a97c56b --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import java.time.Duration; +import java.time.LocalDate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a query parameter of type {@link LocalDate} or a + * dynamic date defined as {@link DateQueryParameterType} with an optional offset. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DateQueryParameter { + + public static final DateQueryParameter EMPTY = new DateQueryParameter(); + + private @Nullable LocalDate date; + private @Nullable Duration offset; + private @Nullable DateQueryParameterType dateType; + + private DateQueryParameter() { + } + + public DateQueryParameter(LocalDate date) { + this.date = date; + } + + public DateQueryParameter(DateQueryParameterType dateType, Duration offset) { + this.dateType = dateType; + this.offset = offset; + } + + public DateQueryParameter(DateQueryParameterType dateType) { + this.dateType = dateType; + } + + @Override + public String toString() { + LocalDate date = this.date; + if (date != null) { + return date.toString(); + } + DateQueryParameterType dateType = this.dateType; + if (dateType != null) { + Duration offset = this.offset; + if (offset == null) { + return dateType.toString(); + } else { + return dateType.toString() + + (offset.isNegative() ? "-" + offset.abs().toString() : "+" + offset.toString()); + } + } + return "null"; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public static DateQueryParameter of(LocalDate localDate) { + return new DateQueryParameter(localDate); + } + + public static DateQueryParameter of(DateQueryParameterType dateType, Duration offset) { + if (offset.isZero()) { + return new DateQueryParameter(dateType); + } else { + return new DateQueryParameter(dateType, offset); + } + } + + public static DateQueryParameter of(DateQueryParameterType dateType) { + return new DateQueryParameter(dateType); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java new file mode 100644 index 0000000000000..3d951b27c8c15 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a dynamic date to be used in a query. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum DateQueryParameterType { + NOW("now"), + UTC_NOW("utcnow"), + START_OF_DAY("StartOfDay"), + START_OF_MONTH("StartOfMonth"), + START_OF_YEAR("StartOfYear"); + + private final String name; + + DateQueryParameterType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java new file mode 100644 index 0000000000000..4baa897b042bc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Global Location Number. + * See {@link https://www.gs1.org/standards/id-keys/gln}} + * The Global Location Number (GLN) can be used by companies to identify their locations. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class GlobalLocationNumber { + + public static final GlobalLocationNumber EMPTY = new GlobalLocationNumber(""); + + private static final int MAX_LENGTH = 13; + + private final String gln; + + public GlobalLocationNumber(String gln) { + if (gln.length() > MAX_LENGTH) { + throw new IllegalArgumentException("Maximum length exceeded: " + gln); + } + this.gln = gln; + } + + @Override + public String toString() { + return gln; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public boolean isValid() { + if (gln.length() != 13) { + return false; + } + + int checksum = 0; + for (int i = 13 - 2; i >= 0; i--) { + int digit = Character.getNumericValue(gln.charAt(i)); + checksum += (i % 2 == 0 ? digit : digit * 3); + } + int controlDigit = 10 - (checksum % 10); + if (controlDigit == 10) { + controlDigit = 0; + } + + return controlDigit == Character.getNumericValue(gln.charAt(13 - 1)); + } + + public static GlobalLocationNumber of(String gln) { + if (gln.isBlank()) { + return EMPTY; + } + return new GlobalLocationNumber(gln); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java new file mode 100644 index 0000000000000..4cbae11721db0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Record as part of {@link DatahubPricelistRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record DatahubPricelistRecord(@SerializedName("ValidFrom") LocalDateTime validFrom, + @SerializedName("ValidTo") LocalDateTime validTo, @SerializedName("ChargeTypeCode") String chargeTypeCode, + @SerializedName("Price1") BigDecimal price1, @SerializedName("Price2") BigDecimal price2, + @SerializedName("Price3") BigDecimal price3, @SerializedName("Price4") BigDecimal price4, + @SerializedName("Price5") BigDecimal price5, @SerializedName("Price6") BigDecimal price6, + @SerializedName("Price7") BigDecimal price7, @SerializedName("Price8") BigDecimal price8, + @SerializedName("Price9") BigDecimal price9, @SerializedName("Price10") BigDecimal price10, + @SerializedName("Price11") BigDecimal price11, @SerializedName("Price12") BigDecimal price12, + @SerializedName("Price13") BigDecimal price13, @SerializedName("Price14") BigDecimal price14, + @SerializedName("Price15") BigDecimal price15, @SerializedName("Price16") BigDecimal price16, + @SerializedName("Price17") BigDecimal price17, @SerializedName("Price18") BigDecimal price18, + @SerializedName("Price19") BigDecimal price19, @SerializedName("Price20") BigDecimal price20, + @SerializedName("Price21") BigDecimal price21, @SerializedName("Price22") BigDecimal price22, + @SerializedName("Price23") BigDecimal price23, @SerializedName("Price24") BigDecimal price24) { + + @Override + public LocalDateTime validTo() { + return Objects.isNull(validTo) ? LocalDateTime.MAX : validTo; + } + + @Override + public BigDecimal price2() { + return Objects.requireNonNullElse(price2, price1()); + } + + @Override + public BigDecimal price3() { + return Objects.requireNonNullElse(price3, price1()); + } + + @Override + public BigDecimal price4() { + return Objects.requireNonNullElse(price4, price1()); + } + + @Override + public BigDecimal price5() { + return Objects.requireNonNullElse(price5, price1()); + } + + @Override + public BigDecimal price6() { + return Objects.requireNonNullElse(price6, price1()); + } + + @Override + public BigDecimal price7() { + return Objects.requireNonNullElse(price7, price1()); + } + + @Override + public BigDecimal price8() { + return Objects.requireNonNullElse(price8, price1()); + } + + @Override + public BigDecimal price9() { + return Objects.requireNonNullElse(price9, price1()); + } + + @Override + public BigDecimal price10() { + return Objects.requireNonNullElse(price10, price1()); + } + + @Override + public BigDecimal price11() { + return Objects.requireNonNullElse(price11, price1()); + } + + @Override + public BigDecimal price12() { + return Objects.requireNonNullElse(price12, price1()); + } + + @Override + public BigDecimal price13() { + return Objects.requireNonNullElse(price13, price1()); + } + + @Override + public BigDecimal price14() { + return Objects.requireNonNullElse(price14, price1()); + } + + @Override + public BigDecimal price15() { + return Objects.requireNonNullElse(price15, price1()); + } + + @Override + public BigDecimal price16() { + return Objects.requireNonNullElse(price16, price1()); + } + + @Override + public BigDecimal price17() { + return Objects.requireNonNullElse(price17, price1()); + } + + @Override + public BigDecimal price18() { + return Objects.requireNonNullElse(price18, price1()); + } + + @Override + public BigDecimal price19() { + return Objects.requireNonNullElse(price19, price1()); + } + + @Override + public BigDecimal price20() { + return Objects.requireNonNullElse(price20, price1()); + } + + @Override + public BigDecimal price21() { + return Objects.requireNonNullElse(price21, price1()); + } + + @Override + public BigDecimal price22() { + return Objects.requireNonNullElse(price22, price1()); + } + + @Override + public BigDecimal price23() { + return Objects.requireNonNullElse(price23, price1()); + } + + @Override + public BigDecimal price24() { + return Objects.requireNonNullElse(price24, price1()); + } + + /** + * Get {@link Map} of tariffs with hour start as key. + * + * @return map with hourly tariffs + */ + public Map getTariffMap() { + Map tariffMap = new HashMap<>(); + + tariffMap.put(LocalTime.of(0, 0), price1()); + tariffMap.put(LocalTime.of(1, 0), price2()); + tariffMap.put(LocalTime.of(2, 0), price3()); + tariffMap.put(LocalTime.of(3, 0), price4()); + tariffMap.put(LocalTime.of(4, 0), price5()); + tariffMap.put(LocalTime.of(5, 0), price6()); + tariffMap.put(LocalTime.of(6, 0), price7()); + tariffMap.put(LocalTime.of(7, 0), price8()); + tariffMap.put(LocalTime.of(8, 0), price9()); + tariffMap.put(LocalTime.of(9, 0), price10()); + tariffMap.put(LocalTime.of(10, 0), price11()); + tariffMap.put(LocalTime.of(11, 0), price12()); + tariffMap.put(LocalTime.of(12, 0), price13()); + tariffMap.put(LocalTime.of(13, 0), price14()); + tariffMap.put(LocalTime.of(14, 0), price15()); + tariffMap.put(LocalTime.of(15, 0), price16()); + tariffMap.put(LocalTime.of(16, 0), price17()); + tariffMap.put(LocalTime.of(17, 0), price18()); + tariffMap.put(LocalTime.of(18, 0), price19()); + tariffMap.put(LocalTime.of(19, 0), price20()); + tariffMap.put(LocalTime.of(20, 0), price21()); + tariffMap.put(LocalTime.of(21, 0), price22()); + tariffMap.put(LocalTime.of(22, 0), price23()); + tariffMap.put(LocalTime.of(23, 0), price24()); + + return tariffMap; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java new file mode 100644 index 0000000000000..4aa6c28a53f65 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Received {@link DatahubPricelistRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record DatahubPricelistRecords(int total, String filters, int limit, String dataset, + DatahubPricelistRecord[] records) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java new file mode 100644 index 0000000000000..a657471c995cc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import java.math.BigDecimal; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Record as part of {@link ElspotpriceRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record ElspotpriceRecord(@SerializedName("HourUTC") Instant hour, + @SerializedName("SpotPriceDKK") BigDecimal spotPriceDKK, + @SerializedName("SpotPriceEUR") BigDecimal spotPriceEUR) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java new file mode 100644 index 0000000000000..87c8c2d71183f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Received {@link ElspotpriceRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record ElspotpriceRecords(int total, String filters, String dataset, ElspotpriceRecord[] records) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java new file mode 100644 index 0000000000000..0591540f45233 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.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 { + + @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 index 0000000000000..76f0b1fafe2c4 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java @@ -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 { + + @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 index 0000000000000..81b24587958e9 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java @@ -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 { + + @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 index 0000000000000..8a59da81db44f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.config; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType; + +/** + * The {@link DatahubPriceConfiguration} class contains fields mapping channel configuration parameters. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DatahubPriceConfiguration { + + /** + * Comma-separated list of charge type codes, e.g. "CD,CD R". + */ + public String chargeTypeCodes = ""; + + /** + * Comma-separated list of notes, e.g. "Nettarif C". + */ + public String notes = ""; + + /** + * Query start date parameter expressed as either yyyy-mm-dd or one of StartOfDay, StartOfMonth or StartOfYear. + */ + public String start = ""; + + /** + * Check if any filter values are provided. + * + * @return true if either charge type codes, notes or query start date is provided. + */ + public boolean hasAnyFilterOverrides() { + return !chargeTypeCodes.isBlank() || !notes.isBlank() || !start.isBlank(); + } + + /** + * Get parsed set of charge type codes from comma-separated string. + * + * @return Set of charge type codes. + */ + public Set getChargeTypeCodes() { + return chargeTypeCodes.isBlank() ? new HashSet<>() + : new HashSet( + Arrays.stream(chargeTypeCodes.split(",")).map(ChargeTypeCode::new).toList()); + } + + /** + * Get parsed set of notes from comma-separated string. + * + * @return Set of notes. + */ + public Set getNotes() { + return notes.isBlank() ? new HashSet<>() : new HashSet(Arrays.asList(notes.split(","))); + } + + /** + * Get query start parameter. + * + * @return null if invalid, otherwise an initialized {@link DateQueryParameter}. + */ + public @Nullable DateQueryParameter getStart() { + if (start.isBlank()) { + return DateQueryParameter.EMPTY; + } + if (start.equals(DateQueryParameterType.START_OF_DAY.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_DAY); + } + if (start.equals(DateQueryParameterType.START_OF_MONTH.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_MONTH); + } + if (start.equals(DateQueryParameterType.START_OF_YEAR.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR); + } + try { + return DateQueryParameter.of(LocalDate.parse(start)); + } catch (DateTimeParseException e) { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java new file mode 100644 index 0000000000000..70cf0b45df7d9 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.config; + +import java.util.Currency; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants; +import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; + +/** + * The {@link EnergiDataServiceConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class EnergiDataServiceConfiguration { + + /** + * Price area (DK1 = West of the Great Belt, DK2 = East of the Great Belt). + */ + public String priceArea = ""; + + /** + * Currency code for the prices. + */ + public String currencyCode = EnergiDataServiceBindingConstants.CURRENCY_DKK.getCurrencyCode(); + + /** + * Global Location Number of the Grid Company. + */ + public String gridCompanyGLN = ""; + + /** + * Global Location Number of Energinet. + */ + public String energinetGLN = "5790000432752"; + + /** + * Get {@link Currency} representing the configured currency code. + * + * @return Currency instance + */ + public Currency getCurrency() { + return Currency.getInstance(currencyCode); + } + + public GlobalLocationNumber getGridCompanyGLN() { + return GlobalLocationNumber.of(gridCompanyGLN); + } + + public GlobalLocationNumber getEnerginetGLN() { + return GlobalLocationNumber.of(energinetGLN); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java new file mode 100644 index 0000000000000..ab2c522e49a84 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link DataServiceException} is a generic Energi Data Service exception thrown in case + * of communication failure or unexpected response. It is intended to be derived by + * specialized exceptions. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DataServiceException extends Exception { + + private static final long serialVersionUID = 1L; + private int httpStatus = 0; + + public DataServiceException(String message) { + super(message); + } + + public DataServiceException(Throwable cause) { + super(cause); + } + + public DataServiceException(String message, Throwable cause) { + super(message, cause); + } + + public DataServiceException(String message, int httpStatus) { + super(message); + this.httpStatus = httpStatus; + } + + public int getHttpStatus() { + return httpStatus; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java new file mode 100644 index 0000000000000..95814c24a0dd5 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link MissingPriceException} is thrown when there are no prices + * available in the requested interval, e.g. when performing a calculation. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class MissingPriceException extends Exception { + + private static final long serialVersionUID = 1L; + + public MissingPriceException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java new file mode 100644 index 0000000000000..4f2f4ef23bd56 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.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 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 index 0000000000000..16bffcf8da388 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java @@ -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> 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 properties = editProperties(); + ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(), + start, properties); + cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency()); + updateProperties(properties); + } + + private void downloadNetTariffs() throws InterruptedException, DataServiceException { + if (config.getGridCompanyGLN().isEmpty()) { + return; + } + if (cacheManager.areNetTariffsValidTomorrow()) { + logger.debug("Cached net tariffs still valid, skipping download."); + cacheManager.updateNetTariffs(); + } else { + cacheManager.putNetTariffs(downloadPriceLists(config.getGridCompanyGLN(), getNetTariffFilter())); + } + } + + private void downloadSystemTariffs() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areSystemTariffsValidTomorrow()) { + logger.debug("Cached system tariffs still valid, skipping download."); + cacheManager.updateSystemTariffs(); + } else { + cacheManager.putSystemTariffs( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getSystemTariff())); + } + } + + private void downloadElectricityTaxes() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areElectricityTaxesValidTomorrow()) { + logger.debug("Cached electricity taxes still valid, skipping download."); + cacheManager.updateElectricityTaxes(); + } else { + cacheManager.putElectricityTaxes( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getElectricityTax())); + } + } + + private void downloadTransmissionNetTariffs() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areTransmissionNetTariffsValidTomorrow()) { + logger.debug("Cached transmission net tariffs still valid, skipping download."); + cacheManager.updateTransmissionNetTariffs(); + } else { + cacheManager.putTransmissionNetTariffs( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getTransmissionNetTariff())); + } + } + + private Collection downloadPriceLists(GlobalLocationNumber globalLocationNumber, + DatahubTariffFilter filter) throws InterruptedException, DataServiceException { + Map properties = editProperties(); + Collection records = apiController.getDatahubPriceLists(globalLocationNumber, + ChargeType.Tariff, filter, properties); + updateProperties(properties); + + return records; + } + + private DatahubTariffFilter getNetTariffFilter() { + Channel channel = getThing().getChannel(CHANNEL_NET_TARIFF); + if (channel == null) { + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration() + .as(DatahubPriceConfiguration.class); + + if (!datahubPriceConfiguration.hasAnyFilterOverrides()) { + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + DateQueryParameter start = datahubPriceConfiguration.getStart(); + if (start == null) { + logger.warn("Invalid channel configuration parameter 'start': {}", datahubPriceConfiguration.start); + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + Set chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes(); + Set notes = datahubPriceConfiguration.getNotes(); + if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) { + // Completely override filter. + return new DatahubTariffFilter(chargeTypeCodes, notes, start); + } else { + // Only override start date in pre-configured filter. + return new DatahubTariffFilter(DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN), start); + } + } + + private void updatePrices() { + cacheManager.cleanup(); + + updateCurrentSpotPrice(); + updateCurrentTariff(CHANNEL_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 spotPriceMap = cacheManager.getSpotPrices(); + Price[] targetPrices = new Price[spotPriceMap.size()]; + List> sourcePrices = spotPriceMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()).toList(); + + int i = 0; + for (Entry sourcePrice : sourcePrices) { + Instant hourStart = sourcePrice.getKey(); + BigDecimal netTariff = cacheManager.getNetTariff(hourStart); + BigDecimal systemTariff = cacheManager.getSystemTariff(hourStart); + BigDecimal electricityTax = cacheManager.getElectricityTax(hourStart); + BigDecimal transmissionNetTariff = cacheManager.getTransmissionNetTariff(hourStart); + targetPrices[i++] = new Price(hourStart.toString(), sourcePrice.getValue(), config.currencyCode, netTariff, + systemTariff, electricityTax, transmissionNetTariff); + } + updateState(CHANNEL_HOURLY_PRICES, new StringType(gson.toJson(targetPrices))); + } + + /** + * Get the configured {@link Currency} for spot prices. + * + * @return Spot price currency + */ + public Currency getCurrency() { + return config.getCurrency(); + } + + /** + * Get cached spot prices or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future spot prices + */ + public Map getSpotPrices() { + try { + downloadSpotPrices(); + } catch (DataServiceException e) { + 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 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 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 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 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 index 0000000000000..00f0ab89cc4e8 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalTime; +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.binding.energidataservice.internal.retry.strategy.ExponentialBackoff; +import org.openhab.binding.energidataservice.internal.retry.strategy.FixedTime; +import org.openhab.binding.energidataservice.internal.retry.strategy.Linear; + +/** + * This factory defines policies for determining appropriate {@link RetryStrategy} based + * on scenario. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class RetryPolicyFactory { + + /** + * Determine {@link RetryStrategy} from {@link Throwable}. + * + * @param e thrown exception + * @return retry strategy + */ + public static RetryStrategy fromThrowable(Throwable e) { + if (e instanceof DataServiceException dse) { + switch (dse.getHttpStatus()) { + case HttpStatus.TOO_MANY_REQUESTS_429: + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(30)); + default: + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); + } + } + + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); + } + + /** + * Default {@link RetryStrategy} with one retry per day. + * This is intended as a dummy strategy until replaced by a concrete one. + * + * @return retry strategy + */ + public static RetryStrategy initial() { + return new Linear().withMinimum(Duration.ofDays(1)); + } + + /** + * Determine {@link RetryStrategy} for next expected data publishing. + * + * @param localTime the time of daily data request in local time-zone + * @param zoneId the local time-zone + * @return retry strategy + */ + public static RetryStrategy atFixedTime(LocalTime localTime, ZoneId zoneId) { + return new FixedTime(localTime, Clock.system(zoneId)).withJitter(1); + } + + /** + * Determine {@link RetryStrategy} when expected spot price data is missing. + * + * @param utcTime the time of daily data request in UTC time-zone + * @return retry strategy + */ + public static RetryStrategy whenExpectedSpotPriceDataMissing(LocalTime localTime, ZoneId zoneId) { + LocalTime now = LocalTime.now(zoneId); + if (now.isAfter(localTime)) { + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(10)).withJitter(0.2); + } + return atFixedTime(localTime, zoneId); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java new file mode 100644 index 0000000000000..eb67ba8300056 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This interface defines a retry strategy for failed network + * requests. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public interface RetryStrategy { + /** + * Get {@link Duration} until next attempt. This will auto-increment number of + * attempts, so should only be called once after each failed request. + * + * @return duration until next attempt according to strategy + */ + Duration getDuration(); +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java new file mode 100644 index 0000000000000..4b510ae22cacc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for exponential backoff with jitter. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ExponentialBackoff implements RetryStrategy { + + private int attempts = 0; + private int factor = 2; + private double jitter = 0.0; + private Duration minimum = Duration.ofMillis(100); + private Duration maximum = Duration.ofHours(6); + + public ExponentialBackoff() { + } + + @Override + public Duration getDuration() { + long minimum = this.minimum.toMillis(); + long maximum = this.maximum.toMillis(); + long duration = minimum * (long) Math.pow(this.factor, this.attempts++); + if (jitter != 0.0) { + double rand = Math.random(); + if ((((int) Math.floor(rand * 10)) & 1) == 0) { + duration += (long) (rand * jitter * duration); + } else { + duration -= (long) (rand * jitter * duration); + } + } + if (duration < minimum) { + duration = minimum; + } + if (duration > maximum) { + duration = maximum; + } + return Duration.ofMillis(duration); + } + + public ExponentialBackoff withFactor(int factor) { + this.factor = factor; + return this; + } + + public ExponentialBackoff withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + public ExponentialBackoff withMinimum(Duration minimum) { + this.minimum = minimum; + return this; + } + + public ExponentialBackoff withMaximum(Duration maximum) { + this.maximum = maximum; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ExponentialBackoff)) { + return false; + } + ExponentialBackoff other = (ExponentialBackoff) o; + + return this.factor == other.factor && this.jitter == other.jitter && this.minimum.equals(other.minimum) + && this.maximum.equals(other.maximum); + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + factor; + result = prime * result + (int) jitter * 100; + result = prime * result + (int) minimum.toMillis(); + result = prime * result + (int) maximum.toMillis(); + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java new file mode 100644 index 0000000000000..98d78a0485a5f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for a fixed time. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class FixedTime implements RetryStrategy { + + private final Clock clock; + + private LocalTime localTime; + private double jitter = 0.0; + + public FixedTime(LocalTime localTime, Clock clock) { + this.localTime = localTime; + this.clock = clock; + } + + @Override + public Duration getDuration() { + LocalTime now = LocalTime.now(clock); + LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(clock), localTime); + if (now.isAfter(localTime)) { + localDateTime = localDateTime.plusDays(1); + } + + Duration base = Duration.between(LocalDateTime.now(clock), localDateTime); + if (jitter == 0.0) { + return base; + } + + long duration = base.toMillis(); + double rand = Math.random(); + duration += (long) (rand * jitter * 1000 * 60); + + return Duration.ofMillis(duration); + } + + public FixedTime withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FixedTime)) { + return false; + } + FixedTime other = (FixedTime) o; + + return this.jitter == other.jitter && this.localTime.equals(other.localTime); + } + + @Override + public final int hashCode() { + final int result = 1; + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java new file mode 100644 index 0000000000000..99bd57a460b7d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for linear retry with jitter. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class Linear implements RetryStrategy { + + private double jitter = 0.0; + private Duration minimum = Duration.ofMillis(100); + private Duration maximum = Duration.ofHours(6); + + public Linear() { + } + + @Override + public Duration getDuration() { + long minimum = this.minimum.toMillis(); + long maximum = this.maximum.toMillis(); + long duration = minimum; + if (jitter != 0.0) { + double rand = Math.random(); + if ((((int) Math.floor(rand * 10)) & 1) == 0) { + duration += (long) (rand * jitter * duration); + } else { + duration -= (long) (rand * jitter * duration); + } + } + if (duration < minimum) { + duration = minimum; + } + if (duration > maximum) { + duration = maximum; + } + return Duration.ofMillis(duration); + } + + public Linear withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + public Linear withMinimum(Duration minimum) { + this.minimum = minimum; + return this; + } + + public Linear withMaximum(Duration maximum) { + this.maximum = maximum; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Linear)) { + return false; + } + Linear other = (Linear) o; + + return this.jitter == other.jitter && this.minimum.equals(other.minimum) && this.maximum.equals(other.maximum); + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) jitter * 100; + result = prime * result + (int) minimum.toMillis(); + result = prime * result + (int) maximum.toMillis(); + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..70d43d22a6a23 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Energi Data Service Binding + This is the binding for Energi Data Service providing open energy data from Energinet. + cloud + dk,no,se + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..77d3dfe3c27c4 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,94 @@ + + + + + + + Price area for spot prices (same as bidding zone). + false + + + + + + + + Currency code in which to obtain spot prices. + DKK + + + + + + + + Global Location Number of the grid company. + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Global Location Number of Energinet. + true + 5790000432752 + + + + + + + Comma-separated list of charge type codes. + true + + + + Comma-separated list of notes. + true + + + + Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, + StartOfMonth or StartOfYear. + false + + + + + + true + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties new file mode 100644 index 0000000000000..8587e9c393ed3 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties @@ -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 index 0000000000000..2a22018260c37 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml @@ -0,0 +1,35 @@ + + + + + + Channels related to electricity + + + + Current spot price in DKK or EUR per kWh. + + + + Current net tariff in DKK per kWh. + + + + Current system tariff in DKK per kWh. + + + + Current electricity tax in DKK per kWh. + + + + Current transmission net tariff in DKK per kWh. + + + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 0000000000000..12d0273c708bd --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,32 @@ + + + + + Number + + Spot price. + Price + + + + + Number + + Datahub price. + Price + + + + + + String + + JSON array with hourly prices from 12 hours ago and onward. + Price + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml new file mode 100644 index 0000000000000..c00115a67ddd7 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml @@ -0,0 +1,19 @@ + + + + + + + This Thing represents the Energi Data Service API. + + + + + + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java new file mode 100644 index 0000000000000..a9016521198fd --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; + +/** + * Tests for {@link CacheManager}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class CacheManagerTest { + + @Test + void areSpotPricesFullyCachedToday() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-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 index 0000000000000..da0b31a78c950 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; +import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Tests for {@link PriceListParser}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class PriceListParserTest { + + private Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) + .create(); + + private T getObjectFromJson(String filename, Class clazz) throws IOException { + try (InputStream inputStream = PriceListParserTest.class.getResourceAsStream(filename)) { + if (inputStream == null) { + throw new IOException("Input stream is null"); + } + byte[] bytes = inputStream.readAllBytes(); + if (bytes == null) { + throw new IOException("Resulting byte-array empty"); + } + String json = new String(bytes, StandardCharsets.UTF_8); + return Objects.requireNonNull(gson.fromJson(json, clazz)); + } + } + + @Test + void toHourlyNoChanges() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-01-23T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(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 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 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 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 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 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 tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(25)); // No records in dataset before 2023-01-01 + for (Instant i = Instant.parse("2022-12-31T23:00:00Z"); i + .isBefore(Instant.parse("2023-01-02T00:00:00Z")); i = i.plus(1, ChronoUnit.HOURS)) { + assertThat(tariffMap.get(i), is(equalTo(new BigDecimal("0.245")))); + } + } + + @Test + void toHourlyDailyTariffs() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-01-28T04:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistTrefor.json", + DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(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 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 index 0000000000000..3170973a0b0ec --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java @@ -0,0 +1,403 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.action; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants; +import org.openhab.binding.energidataservice.internal.PriceListParser; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; +import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer; +import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; +import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +/** + * Tests for {@link EnergiDataServiceActions}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class EnergiDataServiceActionsTest { + + private @NonNullByDefault({}) @Mock EnergiDataServiceHandler handler; + private EnergiDataServiceActions actions = new EnergiDataServiceActions(); + + private Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()).create(); + + private record SpotPrice(Instant hourStart, BigDecimal spotPrice) { + } + + private T getObjectFromJson(String filename, Class clazz) throws IOException { + try (InputStream inputStream = EnergiDataServiceActionsTest.class.getResourceAsStream(filename)) { + if (inputStream == null) { + throw new IOException("Input stream is null"); + } + byte[] bytes = inputStream.readAllBytes(); + if (bytes == null) { + throw new IOException("Resulting byte-array empty"); + } + String json = new String(bytes, StandardCharsets.UTF_8); + return Objects.requireNonNull(gson.fromJson(json, clazz)); + } + } + + @BeforeEach + void setUp() { + final Logger logger = (Logger) LoggerFactory.getLogger(EnergiDataServiceActions.class); + logger.setLevel(Level.OFF); + + actions = new EnergiDataServiceActions(); + } + + @Test + void getPricesSpotPrice() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("SpotPrice"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.992840027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.267680054")))); + } + + @Test + void getPricesNetTariff() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("NetTariff"); + assertThat(actual.size(), is(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 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 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 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 actual = actions.getPrices("SpotPrice,NetTariff"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.425065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.323870054")))); + } + + @Test + void getPricesSpotPriceNetTariffElectricityTax() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("SpotPrice,NetTariff,ElectricityTax"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.433065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.331870054")))); + } + + @Test + void getPricesTotal() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices(); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054")))); + } + + @Test + void getPricesTotalAllElements() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions + .getPrices("spotprice,nettariff,systemtariff,electricitytax,transmissionnettariff"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T15:00:00Z")), is(equalTo(new BigDecimal("1.708765039")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054")))); + } + + @Test + void getPricesInvalidPriceElement() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("spotprice,nettarif"); + assertThat(actual.size(), is(0)); + } + + @Test + void getPricesMixedCurrencies() throws IOException { + mockCommonDatasets(actions); + when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_EUR); + + Map actual = actions.getPrices("spotprice,nettariff"); + assertThat(actual.size(), is(0)); + } + + /** + * Calculate price in period 15:30-16:30 (UTC) with consumption 150 W and the following total prices: + * 15:00:00: 1.708765039 + * 16:00:00: 2.443870054 + * + * Result = (1.708765039 / 2) + (2.443870054 / 2) * 0.150 + * + * @throws IOException + */ + @Test + void calculatePriceSimple() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:30:00Z"), + Instant.parse("2023-02-04T16:30:00Z"), new QuantityType<>(150, Units.WATT)); + assertThat(actual, is(equalTo(new BigDecimal("0.311447631975000000")))); // 0.3114476319750 + } + + /** + * Calculate price in period 15:00-17:00 (UTC) with consumption 1000 W and the following total prices: + * 15:00:00: 1.708765039 + * 16:00:00: 2.443870054 + * + * Result = 1.708765039 + 2.443870054 + * + * @throws IOException + */ + @Test + void calculatePriceFullHours() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:00:00Z"), + Instant.parse("2023-02-04T17:00:00Z"), new QuantityType<>(1, Units.KILOVAR)); + assertThat(actual, is(equalTo(new BigDecimal("4.152635093000000000")))); // 4.152635093 + } + + @Test + void calculatePriceOutOfRangeStart() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-03T23:59:00Z"), + Instant.parse("2023-02-04T12:30:00Z"), new QuantityType<>(1000, Units.WATT)); + assertThat(actual, is(equalTo(BigDecimal.ZERO))); + } + + @Test + void calculatePriceOutOfRangeEnd() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-05T22:00:00Z"), + Instant.parse("2023-02-05T23:01:00Z"), new QuantityType<>(1000, Units.WATT)); + assertThat(actual, is(equalTo(BigDecimal.ZERO))); + } + + /** + * Miele G 6895 SCVi XXL K2O dishwasher, program ECO. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodWithPowerDishwasher() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4), + Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41), + Duration.ofMinutes(104)); + List> consumptions = List.of(QuantityType.valueOf(162.162162, Units.WATT), + QuantityType.valueOf(750, Units.WATT), QuantityType.valueOf(1500, Units.WATT), + QuantityType.valueOf(3000, Units.WATT), QuantityType.valueOf(1500, Units.WATT), + QuantityType.valueOf(166.666666, Units.WATT), QuantityType.valueOf(146.341463, Units.WATT), + QuantityType.valueOf(0, Units.WATT)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), durations, consumptions); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithPowerOutOfRange() throws IOException { + mockCommonDatasets(actions); + + List durations = List.of(Duration.ofMinutes(61)); + List> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"), + Instant.parse("2023-02-06T00:01:00Z"), durations, consumptions); + assertThat(actual.size(), is(equalTo(0))); + } + + /** + * Miele G 6895 SCVi XXL K2O dishwasher, program ECO. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodWithEnergyDishwasher() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4), + Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236), durations, + QuantityType.valueOf(0.1, Units.KILOWATT_HOUR)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithEnergyTotalDurationIsExactSum() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(120), durations, + QuantityType.valueOf(100, Units.WATT_HOUR)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("0.293540001200000000")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithEnergyTotalDurationInvalid() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(119), durations, + QuantityType.valueOf(0.1, Units.KILOWATT_HOUR)); + assertThat(actual.size(), is(equalTo(0))); + } + + /** + * Like {@link #calculateCheapestPeriodWithEnergyDishwasher} but with unknown consumption/timetable map. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodAssumingLinearUnknownConsumption() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236)); + assertThat(actual.get("LowestPrice"), is(nullValue())); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z")))); + assertThat(actual.get("HighestPrice"), is(nullValue())); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodForLinearPowerUsage() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"), + Instant.parse("2023-02-05T23:00:00Z"), Duration.ofMinutes(61), QuantityType.valueOf(1000, Units.WATT)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.323990859575000000")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T12:00:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("2.589061780353348000")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-04T17:00:00Z")))); + } + + private void mockCommonDatasets(EnergiDataServiceActions actions) throws IOException { + mockCommonDatasets(actions, "SpotPrices20230204.json"); + } + + private void mockCommonDatasets(EnergiDataServiceActions actions, String spotPricesFilename) throws IOException { + SpotPrice[] spotPriceRecords = getObjectFromJson(spotPricesFilename, SpotPrice[].class); + Map spotPrices = Arrays.stream(spotPriceRecords) + .collect(Collectors.toMap(SpotPrice::hourStart, SpotPrice::spotPrice)); + + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(spotPriceRecords[0].hourStart, EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords datahubRecords = getObjectFromJson("NetTariffs.json", DatahubPricelistRecords.class); + Map netTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class); + Map systemTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class); + Map electricityTaxes = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class); + Map transmissionNetTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + + when(handler.getSpotPrices()).thenReturn(spotPrices); + when(handler.getNetTariffs()).thenReturn(netTariffs); + when(handler.getSystemTariffs()).thenReturn(systemTariffs); + when(handler.getElectricityTaxes()).thenReturn(electricityTaxes); + when(handler.getTransmissionNetTariffs()).thenReturn(transmissionNetTariffs); + when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_DKK); + actions.setThingHandler(handler); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java new file mode 100644 index 0000000000000..9334b3d903c47 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.time.Duration; +import java.time.LocalDate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for {@link DateQueryParameter}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class DateQueryParameterTest { + + @Test + void dateQueryParameterTypeWithNegativeOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(-12)); + assertThat(parameter.toString(), is(equalTo("utcnow-PT12H"))); + } + + @Test + void dateQueryParameterTypeWithPositiveOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(12)); + assertThat(parameter.toString(), is(equalTo("utcnow+PT12H"))); + } + + @Test + void dateQueryParameterTypeWithZeroOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ZERO); + assertThat(parameter.toString(), is(equalTo("utcnow"))); + } + + @Test + void dateQueryParameterTypeWithoutOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.NOW); + assertThat(parameter.toString(), is(equalTo("now"))); + } + + @Test + void localDate() { + DateQueryParameter parameter = DateQueryParameter.of(LocalDate.of(2023, 2, 28)); + assertThat(parameter.toString(), is(equalTo("2023-02-28"))); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java new file mode 100644 index 0000000000000..15e135e2c38a6 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for {@link GlobalLocationNumber}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class GlobalLocationNumberTest { + + @Test + void isValid() { + assertThat(GlobalLocationNumber.of("5790000682102").isValid(), is(true)); + } + + @Test + void isInvalid() { + assertThat(GlobalLocationNumber.of("5790000682103").isValid(), is(false)); + } + + @Test + void emptyIsInvalid() { + assertThat(GlobalLocationNumber.EMPTY.isValid(), is(false)); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/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 index 0000000000000..92a9c23c512f2 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializerTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.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 index 0000000000000..116a8a87a79d0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializerTest.java @@ -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 index 0000000000000..90a6c43177a1f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link ExponentialBackoff}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ExponentialBackoffTest { + + @Test + void exponential() { + RetryStrategy retryPolicy = new ExponentialBackoff().withMinimum(Duration.ofSeconds(2)).withJitter(0.0); + for (long i = 2; i <= 256; i *= 2) { + assertThat(retryPolicy.getDuration().toSeconds(), is(i)); + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java new file mode 100644 index 0000000000000..22f4b21a75364 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link FixedTime}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class FixedTimeTest { + + @Test + void beforeNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T10:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ofHours(2))); + } + + @Test + void atNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T12:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ZERO)); + } + + @Test + void afterNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T13:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ofHours(23))); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java new file mode 100644 index 0000000000000..4d8b6abe2a8a9 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link Linear}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class LinearTest { + + @Test + void linear() { + RetryStrategy retryPolicy = new Linear().withMinimum(Duration.ofMinutes(1)).withJitter(0.0); + for (int i = 0; i <= 10; i++) { + assertThat(retryPolicy.getDuration(), is(Duration.ofMinutes(1))); + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json new file mode 100644 index 0000000000000..fce1f184d3c36 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json @@ -0,0 +1,83 @@ +{ + "total": 2, + "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-07-01T00:00:00", + "ValidTo": null, + "VATClass": "D02", + "Price1": 0.697, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + }, + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-07-01T00:00:00", + "VATClass": "D02", + "Price1": 0.008, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json new file mode 100644 index 0000000000000..8d265147a1fbc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json @@ -0,0 +1,588 @@ +{ + "total": 20, + "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-04-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 0.432225, + "Price19": 0.432225, + "Price20": 0.432225, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + }, + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 1.05619, + "Price19": 1.05619, + "Price20": 1.05619, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + }, + { + "ValidFrom": "2022-11-01T00:00:00", + "ValidTo": "2023-01-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.407717, + "Price2": 0.407717, + "Price3": 0.407717, + "Price4": 0.407717, + "Price5": 0.407717, + "Price6": 0.407717, + "Price7": 0.407717, + "Price8": 0.407717, + "Price9": 0.407717, + "Price10": 0.407717, + "Price11": 0.407717, + "Price12": 0.407717, + "Price13": 0.407717, + "Price14": 0.407717, + "Price15": 0.407717, + "Price16": 0.407717, + "Price17": 0.407717, + "Price18": 1.015888, + "Price19": 1.015888, + "Price20": 1.015888, + "Price21": 0.407717, + "Price22": 0.407717, + "Price23": 0.407717, + "Price24": 0.407717 + }, + { + "ValidFrom": "2022-10-01T00:00:00", + "ValidTo": "2022-11-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.31535, + "Price2": 0.31535, + "Price3": 0.31535, + "Price4": 0.31535, + "Price5": 0.31535, + "Price6": 0.31535, + "Price7": 0.31535, + "Price8": 0.31535, + "Price9": 0.31535, + "Price10": 0.31535, + "Price11": 0.31535, + "Price12": 0.31535, + "Price13": 0.31535, + "Price14": 0.31535, + "Price15": 0.31535, + "Price16": 0.31535, + "Price17": 0.31535, + "Price18": 0.821619, + "Price19": 0.821619, + "Price20": 0.821619, + "Price21": 0.31535, + "Price22": 0.31535, + "Price23": 0.31535, + "Price24": 0.31535 + }, + { + "ValidFrom": "2022-08-01T00:00:00", + "ValidTo": "2022-10-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.31535, + "Price2": 0.31535, + "Price3": 0.31535, + "Price4": 0.31535, + "Price5": 0.31535, + "Price6": 0.31535, + "Price7": 0.31535, + "Price8": 0.31535, + "Price9": 0.31535, + "Price10": 0.31535, + "Price11": 0.31535, + "Price12": 0.31535, + "Price13": 0.31535, + "Price14": 0.31535, + "Price15": 0.31535, + "Price16": 0.31535, + "Price17": 0.31535, + "Price18": 0.31535, + "Price19": 0.31535, + "Price20": 0.31535, + "Price21": 0.31535, + "Price22": 0.31535, + "Price23": 0.31535, + "Price24": 0.31535 + }, + { + "ValidFrom": "2022-04-01T00:00:00", + "ValidTo": "2022-08-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.227969, + "Price2": 0.227969, + "Price3": 0.227969, + "Price4": 0.227969, + "Price5": 0.227969, + "Price6": 0.227969, + "Price7": 0.227969, + "Price8": 0.227969, + "Price9": 0.227969, + "Price10": 0.227969, + "Price11": 0.227969, + "Price12": 0.227969, + "Price13": 0.227969, + "Price14": 0.227969, + "Price15": 0.227969, + "Price16": 0.227969, + "Price17": 0.227969, + "Price18": 0.227969, + "Price19": 0.227969, + "Price20": 0.227969, + "Price21": 0.227969, + "Price22": 0.227969, + "Price23": 0.227969, + "Price24": 0.227969 + }, + { + "ValidFrom": "2022-01-01T00:00:00", + "ValidTo": "2022-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.183226, + "Price2": 0.183226, + "Price3": 0.183226, + "Price4": 0.183226, + "Price5": 0.183226, + "Price6": 0.183226, + "Price7": 0.183226, + "Price8": 0.183226, + "Price9": 0.183226, + "Price10": 0.183226, + "Price11": 0.183226, + "Price12": 0.183226, + "Price13": 0.183226, + "Price14": 0.183226, + "Price15": 0.183226, + "Price16": 0.183226, + "Price17": 0.183226, + "Price18": 0.543732, + "Price19": 0.543732, + "Price20": 0.543732, + "Price21": 0.183226, + "Price22": 0.183226, + "Price23": 0.183226, + "Price24": 0.183226 + }, + { + "ValidFrom": "2021-10-01T00:00:00", + "ValidTo": "2022-01-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.5448, + "Price19": 0.5448, + "Price20": 0.5448, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2021-04-01T00:00:00", + "ValidTo": "2021-10-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.1717, + "Price19": 0.1717, + "Price20": 0.1717, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2021-01-01T00:00:00", + "ValidTo": "2021-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.5448, + "Price19": 0.5448, + "Price20": 0.5448, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "CD R", + "Price1": 0.0, + "Price2": 0.0, + "Price3": 0.0, + "Price4": 0.0, + "Price5": 0.0, + "Price6": 0.0, + "Price7": 0.0, + "Price8": 0.0, + "Price9": 0.0, + "Price10": 0.0, + "Price11": 0.0, + "Price12": 0.0, + "Price13": 0.0, + "Price14": 0.0, + "Price15": 0.0, + "Price16": 0.0, + "Price17": 0.0, + "Price18": 0.0, + "Price19": 0.0, + "Price20": 0.0, + "Price21": 0.0, + "Price22": 0.0, + "Price23": 0.0, + "Price24": 0.0 + }, + { + "ValidFrom": "2022-12-01T00:00:00", + "ValidTo": "2023-01-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.407717, + "Price2": -0.407717, + "Price3": -0.407717, + "Price4": -0.407717, + "Price5": -0.407717, + "Price6": -0.407717, + "Price7": -0.407717, + "Price8": -0.407717, + "Price9": -0.407717, + "Price10": -0.407717, + "Price11": -0.407717, + "Price12": -0.407717, + "Price13": -0.407717, + "Price14": -0.407717, + "Price15": -0.407717, + "Price16": -0.407717, + "Price17": -0.407717, + "Price18": -1.015888, + "Price19": -1.015888, + "Price20": -1.015888, + "Price21": -0.407717, + "Price22": -0.407717, + "Price23": -0.407717, + "Price24": -0.407717 + }, + { + "ValidFrom": "2022-10-01T00:00:00", + "ValidTo": "2022-12-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.042484, + "Price19": -0.042484, + "Price20": -0.042484, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-08-01T00:00:00", + "ValidTo": "2022-10-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.0202, + "Price19": -0.0202, + "Price20": -0.0202, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-04-01T00:00:00", + "ValidTo": "2022-08-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.0202, + "Price19": -0.0202, + "Price20": -0.0202, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-01-01T00:00:00", + "ValidTo": "2022-04-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.042484, + "Price19": -0.042484, + "Price20": -0.042484, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2021-10-01T00:00:00", + "ValidTo": "2022-01-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-04-01T00:00:00", + "ValidTo": "2021-10-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0224, + "Price19": -0.0224, + "Price20": -0.0224, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-03-01T00:00:00", + "ValidTo": "2021-04-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-01-01T00:00:00", + "ValidTo": "2021-03-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0471, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json new file mode 100644 index 0000000000000..7641d136c6d4d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json @@ -0,0 +1,37 @@ +{ + "total": 1, + "filters": "{\"Note\":[\"Nettarif C\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000610877\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "TA031U200", + "Price1": 0.245, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json new file mode 100644 index 0000000000000..e0610a8caf8e8 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json @@ -0,0 +1,2908 @@ +{ + "total": 850, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000706686\"],\"ChargeTypeCode\":[\"46\"],\"Note\":[\"Nettarif C time\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-04-30T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-29T00:00:00", + "ValidTo": "2023-04-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-28T00:00:00", + "ValidTo": "2023-04-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-27T00:00:00", + "ValidTo": "2023-04-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-26T00:00:00", + "ValidTo": "2023-04-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-25T00:00:00", + "ValidTo": "2023-04-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-24T00:00:00", + "ValidTo": "2023-04-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-23T00:00:00", + "ValidTo": "2023-04-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-22T00:00:00", + "ValidTo": "2023-04-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-21T00:00:00", + "ValidTo": "2023-04-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-20T00:00:00", + "ValidTo": "2023-04-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-19T00:00:00", + "ValidTo": "2023-04-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-18T00:00:00", + "ValidTo": "2023-04-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-17T00:00:00", + "ValidTo": "2023-04-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-16T00:00:00", + "ValidTo": "2023-04-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-15T00:00:00", + "ValidTo": "2023-04-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-14T00:00:00", + "ValidTo": "2023-04-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-13T00:00:00", + "ValidTo": "2023-04-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-12T00:00:00", + "ValidTo": "2023-04-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-11T00:00:00", + "ValidTo": "2023-04-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-10T00:00:00", + "ValidTo": "2023-04-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-09T00:00:00", + "ValidTo": "2023-04-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-08T00:00:00", + "ValidTo": "2023-04-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-07T00:00:00", + "ValidTo": "2023-04-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-06T00:00:00", + "ValidTo": "2023-04-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-05T00:00:00", + "ValidTo": "2023-04-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-04T00:00:00", + "ValidTo": "2023-04-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-03T00:00:00", + "ValidTo": "2023-04-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-02T00:00:00", + "ValidTo": "2023-04-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-01T00:00:00", + "ValidTo": "2023-04-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-03-31T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-30T00:00:00", + "ValidTo": "2023-03-31T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-29T00:00:00", + "ValidTo": "2023-03-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-28T00:00:00", + "ValidTo": "2023-03-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-27T00:00:00", + "ValidTo": "2023-03-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-26T00:00:00", + "ValidTo": "2023-03-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-25T00:00:00", + "ValidTo": "2023-03-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-24T00:00:00", + "ValidTo": "2023-03-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-23T00:00:00", + "ValidTo": "2023-03-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-22T00:00:00", + "ValidTo": "2023-03-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-21T00:00:00", + "ValidTo": "2023-03-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-20T00:00:00", + "ValidTo": "2023-03-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-19T00:00:00", + "ValidTo": "2023-03-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-18T00:00:00", + "ValidTo": "2023-03-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-17T00:00:00", + "ValidTo": "2023-03-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-16T00:00:00", + "ValidTo": "2023-03-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-15T00:00:00", + "ValidTo": "2023-03-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-14T00:00:00", + "ValidTo": "2023-03-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-13T00:00:00", + "ValidTo": "2023-03-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-12T00:00:00", + "ValidTo": "2023-03-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-11T00:00:00", + "ValidTo": "2023-03-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-10T00:00:00", + "ValidTo": "2023-03-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-09T00:00:00", + "ValidTo": "2023-03-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-08T00:00:00", + "ValidTo": "2023-03-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-07T00:00:00", + "ValidTo": "2023-03-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-06T00:00:00", + "ValidTo": "2023-03-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-05T00:00:00", + "ValidTo": "2023-03-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-04T00:00:00", + "ValidTo": "2023-03-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-03T00:00:00", + "ValidTo": "2023-03-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-02T00:00:00", + "ValidTo": "2023-03-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-01T00:00:00", + "ValidTo": "2023-03-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-28T00:00:00", + "ValidTo": "2023-03-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-27T00:00:00", + "ValidTo": "2023-02-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-26T00:00:00", + "ValidTo": "2023-02-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-25T00:00:00", + "ValidTo": "2023-02-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-24T00:00:00", + "ValidTo": "2023-02-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-23T00:00:00", + "ValidTo": "2023-02-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-22T00:00:00", + "ValidTo": "2023-02-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-21T00:00:00", + "ValidTo": "2023-02-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-20T00:00:00", + "ValidTo": "2023-02-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-19T00:00:00", + "ValidTo": "2023-02-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-18T00:00:00", + "ValidTo": "2023-02-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-17T00:00:00", + "ValidTo": "2023-02-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-16T00:00:00", + "ValidTo": "2023-02-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-15T00:00:00", + "ValidTo": "2023-02-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-14T00:00:00", + "ValidTo": "2023-02-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-13T00:00:00", + "ValidTo": "2023-02-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-12T00:00:00", + "ValidTo": "2023-02-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-11T00:00:00", + "ValidTo": "2023-02-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-10T00:00:00", + "ValidTo": "2023-02-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-09T00:00:00", + "ValidTo": "2023-02-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-08T00:00:00", + "ValidTo": "2023-02-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-07T00:00:00", + "ValidTo": "2023-02-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-06T00:00:00", + "ValidTo": "2023-02-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-05T00:00:00", + "ValidTo": "2023-02-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-04T00:00:00", + "ValidTo": "2023-02-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-03T00:00:00", + "ValidTo": "2023-02-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-02T00:00:00", + "ValidTo": "2023-02-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-01T00:00:00", + "ValidTo": "2023-02-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-31T00:00:00", + "ValidTo": "2023-02-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-30T00:00:00", + "ValidTo": "2023-01-31T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-29T00:00:00", + "ValidTo": "2023-01-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-28T00:00:00", + "ValidTo": "2023-01-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-27T00:00:00", + "ValidTo": "2023-01-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-26T00:00:00", + "ValidTo": "2023-01-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-25T00:00:00", + "ValidTo": "2023-01-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-24T00:00:00", + "ValidTo": "2023-01-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-23T00:00:00", + "ValidTo": "2023-01-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-22T00:00:00", + "ValidTo": "2023-01-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-21T00:00:00", + "ValidTo": "2023-01-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json new file mode 100644 index 0000000000000..5ee45ea673045 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json @@ -0,0 +1,45 @@ +{ + "total": 1, + "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-07-01T00:00:00", + "VATClass": "D02", + "Price1": 0.008, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json new file mode 100644 index 0000000000000..1af9754e1a6b0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json @@ -0,0 +1,37 @@ +{ + "total": 1, + "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 1.05619, + "Price19": 1.05619, + "Price20": 1.05619, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json new file mode 100644 index 0000000000000..76423da41996e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json @@ -0,0 +1,142 @@ +[ + { + "hourStart": "2023-02-04T12:00:00Z", + "spotPrice": 0.992840027 + }, + { + "hourStart": "2023-02-04T13:00:00Z", + "spotPrice": 0.998200012 + }, + { + "hourStart": "2023-02-04T14:00:00Z", + "spotPrice": 1.054180054 + }, + { + "hourStart": "2023-02-04T15:00:00Z", + "spotPrice": 1.156540039 + }, + { + "hourStart": "2023-02-04T16:00:00Z", + "spotPrice": 1.267680054 + }, + { + "hourStart": "2023-02-04T17:00:00Z", + "spotPrice": 1.370939941 + }, + { + "hourStart": "2023-02-04T18:00:00Z", + "spotPrice": 1.339670044 + }, + { + "hourStart": "2023-02-04T19:00:00Z", + "spotPrice": 1.24973999 + }, + { + "hourStart": "2023-02-04T20:00:00Z", + "spotPrice": 1.177160034 + }, + { + "hourStart": "2023-02-04T21:00:00Z", + "spotPrice": 0.979809998 + }, + { + "hourStart": "2023-02-04T22:00:00Z", + "spotPrice": 0.804200012 + }, + { + "hourStart": "2023-02-04T23:00:00Z", + "spotPrice": 0.82826001 + }, + { + "hourStart": "2023-02-05T00:00:00Z", + "spotPrice": 0.777280029 + }, + { + "hourStart": "2023-02-05T01:00:00Z", + "spotPrice": 0.771549988 + }, + { + "hourStart": "2023-02-05T02:00:00Z", + "spotPrice": 0.757559998 + }, + { + "hourStart": "2023-02-05T03:00:00Z", + "spotPrice": 0.751599976 + }, + { + "hourStart": "2023-02-05T04:00:00Z", + "spotPrice": 0.76373999 + }, + { + "hourStart": "2023-02-05T05:00:00Z", + "spotPrice": 0.764700012 + }, + { + "hourStart": "2023-02-05T06:00:00Z", + "spotPrice": 0.784650024 + }, + { + "hourStart": "2023-02-05T07:00:00Z", + "spotPrice": 0.79551001 + }, + { + "hourStart": "2023-02-05T08:00:00Z", + "spotPrice": 0.805789978 + }, + { + "hourStart": "2023-02-05T09:00:00Z", + "spotPrice": 0.807789978 + }, + { + "hourStart": "2023-02-05T10:00:00Z", + "spotPrice": 0.796849976 + }, + { + "hourStart": "2023-02-05T11:00:00Z", + "spotPrice": 0.756289978 + }, + { + "hourStart": "2023-02-05T12:00:00Z", + "spotPrice": 0.749369995 + }, + { + "hourStart": "2023-02-05T13:00:00Z", + "spotPrice": 0.7915 + }, + { + "hourStart": "2023-02-05T14:00:00Z", + "spotPrice": 0.838830017 + }, + { + "hourStart": "2023-02-05T15:00:00Z", + "spotPrice": 0.892859985 + }, + { + "hourStart": "2023-02-05T16:00:00Z", + "spotPrice": 1.01997998 + }, + { + "hourStart": "2023-02-05T17:00:00Z", + "spotPrice": 0.99452002 + }, + { + "hourStart": "2023-02-05T18:00:00Z", + "spotPrice": 0.976140015 + }, + { + "hourStart": "2023-02-05T19:00:00Z", + "spotPrice": 0.923669983 + }, + { + "hourStart": "2023-02-05T20:00:00Z", + "spotPrice": 0.906700012 + }, + { + "hourStart": "2023-02-05T21:00:00Z", + "spotPrice": 0.931859985 + }, + { + "hourStart": "2023-02-05T22:00:00Z", + "spotPrice": 0.941159973 + } +] diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json new file mode 100644 index 0000000000000..ce12925c942eb --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json @@ -0,0 +1,142 @@ +[ + { + "hourStart": "2023-02-05T12:00:00Z", + "spotPrice": 0.749609985 + }, + { + "hourStart": "2023-02-05T13:00:00Z", + "spotPrice": 0.79173999 + }, + { + "hourStart": "2023-02-05T14:00:00Z", + "spotPrice": 0.839090027 + }, + { + "hourStart": "2023-02-05T15:00:00Z", + "spotPrice": 0.893140015 + }, + { + "hourStart": "2023-02-05T16:00:00Z", + "spotPrice": 1.020299988 + }, + { + "hourStart": "2023-02-05T17:00:00Z", + "spotPrice": 0.994840027 + }, + { + "hourStart": "2023-02-05T18:00:00Z", + "spotPrice": 0.976450012 + }, + { + "hourStart": "2023-02-05T19:00:00Z", + "spotPrice": 0.923960022 + }, + { + "hourStart": "2023-02-05T20:00:00Z", + "spotPrice": 0.90698999 + }, + { + "hourStart": "2023-02-05T21:00:00Z", + "spotPrice": 0.932150024 + }, + { + "hourStart": "2023-02-05T22:00:00Z", + "spotPrice": 0.941460022 + }, + { + "hourStart": "2023-02-05T23:00:00Z", + "spotPrice": 1.07947998 + }, + { + "hourStart": "2023-02-06T00:00:00Z", + "spotPrice": 1.070030029 + }, + { + "hourStart": "2023-02-06T01:00:00Z", + "spotPrice": 1.082540039 + }, + { + "hourStart": "2023-02-06T02:00:00Z", + "spotPrice": 1.057819946 + }, + { + "hourStart": "2023-02-06T03:00:00Z", + "spotPrice": 1.0430 + }, + { + "hourStart": "2023-02-06T04:00:00Z", + "spotPrice": 1.10873999 + }, + { + "hourStart": "2023-02-06T05:00:00Z", + "spotPrice": 1.307810059 + }, + { + "hourStart": "2023-02-06T06:00:00Z", + "spotPrice": 1.493780029 + }, + { + "hourStart": "2023-02-06T07:00:00Z", + "spotPrice": 1.588630005 + }, + { + "hourStart": "2023-02-06T08:00:00Z", + "spotPrice": 1.493780029 + }, + { + "hourStart": "2023-02-06T09:00:00Z", + "spotPrice": 1.377869995 + }, + { + "hourStart": "2023-02-06T10:00:00Z", + "spotPrice": 1.338859985 + }, + { + "hourStart": "2023-02-06T11:00:00Z", + "spotPrice": 1.256069946 + }, + { + "hourStart": "2023-02-06T12:00:00Z", + "spotPrice": 1.199790039 + }, + { + "hourStart": "2023-02-06T13:00:00Z", + "spotPrice": 1.220189941 + }, + { + "hourStart": "2023-02-06T14:00:00Z", + "spotPrice": 1.270589966 + }, + { + "hourStart": "2023-02-06T15:00:00Z", + "spotPrice": 1.353449951 + }, + { + "hourStart": "2023-02-06T16:00:00Z", + "spotPrice": 1.481050049 + }, + { + "hourStart": "2023-02-06T17:00:00Z", + "spotPrice": 1.589449951 + }, + { + "hourStart": "2023-02-06T18:00:00Z", + "spotPrice": 1.52898999 + }, + { + "hourStart": "2023-02-06T19:00:00Z", + "spotPrice": 1.386280029 + }, + { + "hourStart": "2023-02-06T20:00:00Z", + "spotPrice": 1.239400024 + }, + { + "hourStart": "2023-02-06T21:00:00Z", + "spotPrice": 1.135319946 + }, + { + "hourStart": "2023-02-06T22:00:00Z", + "spotPrice": 1.14648999 + } +] diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json new file mode 100644 index 0000000000000..8b1d42b98391f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json @@ -0,0 +1,36 @@ +{ + "total": 1, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Systemtarif\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "41000", + "Price1": 0.054, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json new file mode 100644 index 0000000000000..c2ed993403483 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json @@ -0,0 +1,36 @@ +{ + "total": 1, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Transmissions nettarif\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "40000", + "Price1": 0.058, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/pom.xml b/bundles/pom.xml index cf42108c5e82b..4a2b07e052c79 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -125,6 +125,7 @@ org.openhab.binding.elerotransmitterstick org.openhab.binding.elroconnects org.openhab.binding.energenie + org.openhab.binding.energidataservice org.openhab.binding.enigma2 org.openhab.binding.enocean org.openhab.binding.enphase