From c095beebf9e4752ae5e860419ce1a26af92fb8ab Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 15 Jan 2023 01:01:46 +0100 Subject: [PATCH 01/17] Initial contribution Signed-off-by: Jacob Laursen --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 13 + .../README.md | 408 +++ .../pom.xml | 25 + .../src/main/feature/feature.xml | 9 + .../internal/ApiController.java | 254 ++ .../internal/CacheManager.java | 443 +++ .../EnergiDataServiceBindingConstants.java | 80 + .../internal/PriceCalculator.java | 233 ++ .../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 | 165 + .../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 | 42 + .../serialization/LocalDateDeserializer.java | 39 + .../LocalDateTimeDeserializer.java | 39 + .../config/DatahubPriceConfiguration.java | 103 + .../EnergiDataServiceConfiguration.java | 65 + .../internal/config/PriceConfiguration.java | 32 + .../exception/DataServiceException.java | 50 + .../exception/MissingPriceException.java | 31 + .../EnergiDataServiceHandlerFactory.java | 77 + .../handler/EnergiDataServiceHandler.java | 561 ++++ .../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 | 104 + .../OH-INF/i18n/energidataservice.properties | 108 + .../resources/OH-INF/thing/channel-groups.xml | 35 + .../resources/OH-INF/thing/channel-types.xml | 33 + .../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 + .../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 + 63 files changed, 9538 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/config/PriceConfiguration.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/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 899c818aa135f..ec8406ecc3420 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -91,6 +91,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 54802e935c9db..eb144a57026a7 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -451,6 +451,11 @@ org.openhab.binding.energenie ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.energidataservice + ${project.version} + org.openhab.addons.bundles org.openhab.binding.enigma2 diff --git a/bundles/org.openhab.binding.energidataservice/NOTICE b/bundles/org.openhab.binding.energidataservice/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md new file mode 100644 index 0000000000000..b49b2c8e58b15 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -0,0 +1,408 @@ +# Energi Data Service Binding + +This binding integrates electricity prices from the Danish Energi Data Service ("Open energy data from Energinet to society"). + +This can be used to plan energy consumption, for example to calculate the cheapest period for running a dishwasher or charging an EV. + +## Supported Things + +All channels are available for thing type `service`. + +## Thing Configuration + +### `service` Thing Configuration + +| Name | Type | Description | Default | Required | +|----------------|---------|---------------------------------------------------|---------------|----------| +| priceArea | text | Price area for spot prices (same as bidding zone) | | yes | +| currencyCode | text | Currency code in which to obtain spot prices | DKK | no | +| gridCompanyGLN | integer | Global Location Number of the Grid Company | | no | +| energinetGLN | integer | Global Location Number of Energinet | 5790000432752 | no | + +#### Global Location Number of the Grid Company + +The Global Location Number of your grid company can be selected from a built-in list of grid companies. +To find the company in your area, you can go to [Find netselskab](https://greenpowerdenmark.dk/vejledning-teknik/nettilslutning/find-netselskab), enter your address, and the company will be shown. + +If your company is not on the list, you can configure it manually. +To obtain the Global Location Number of your grid company: + +- Open a browser and go to [Eloverblik](https://eloverblik.dk/). +- Click "Private customers" and log in with MitID (confirmation will appear as Energinet). +- Click "Retrieve data" and select "Price data". +- Open the file and look for the rows having **Price_type** = "Subscription". +- In the columns **Name** and/or **Description** you should see the name of your grid company. +- In column **Owner** you can find the GLN ("Global Location Number"). +- Most rows will have this **Owner**. If in doubt, try to look for rows __not__ having 5790000432752 as owner. + +## Channels + +### Channel Group `electricity` + +| Channel | Type | Description | Advanced | +|------------------------------|--------|------------------------------------------------------------------------------------------------|----------| +| currentSpotPrice | Number | Spot price in DKK or EUR per kWh for current hour | no | +| currentNetTariff | Number | Net tariff in DKK per kWh for current hour. Only available when `gridCompanyGLN` is configured | no | +| currentSystemTariff | Number | System tariff in DKK per kWh for current hour | no | +| currentElectricityTax | Number | Electricity tax in DKK per kWh for current hour | no | +| currentTransmissionNetTariff | Number | Transmission net tariff in DKK per kWh for current hour | no | +| hourlyPrices | String | JSON array with hourly prices from 12 hours ago and onward | yes | + +_Please note:_ There is no channel providing the total price. +Instead, create a group item with `SUM` as aggregate function and add the individual price items as children. +This has the following advantages: + +- Full customization possible: Freely choose the channels which should be included in the total. +- An additional item containing the kWh fee from your electricity supplier can be added also. +- Spot price can be configured in EUR while tariffs are in DKK. + +#### Value-Added Tax + +The channels `currentSpotPrice`, `currentNetTariff`, `currentSystemTariff`, `currentElectricityTax` and `currentTransmissionNetTariff` can be configured to include VAT with this configuration parameter: + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|----------------------------------------------|---------|----------|----------| +| includeVAT | boolean | Add VAT to amount based on regional settings | no | no | no | + +Please be aware that this channel configuration will affect all linked items. + +#### Current Net Tariff + +Discounts are automatically taken into account for channel `currentNetTariff` so that it represents the actual price. + +The tariffs are downloaded using pre-configured filters for the different [Grid Company GLN's](#global-location-number-of-the-grid-company). +If your company is not in the list, or the filters are not working, they can be manually overridden. +To override filters, the channel `currentNetTariff` has the following configuration parameters: + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|----------------------------------------------------------------------------------------------------------------------------|---------|----------|----------| +| chargeTypeCodes | text | Comma-separated list of charge type codes | | no | yes | +| notes | text | Comma-separated list of notes | | no | yes | +| start | text | Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear | | no | yes | + +The parameters `chargeTypeCodes` and `notes` are logically combined with "AND", so if only one parameter is needed for filter, only provide this parameter and leave the other one empty. +Using any of these parameters will override the pre-configured filter entirely. + +The parameter `start` can be used independently to override the query start date parameter. +If used while leaving `chargeTypeCodes` and `notes` empty, only the date will be overridden. + +Determining the right filters can be tricky, so if in doubt ask in the community forum. +See also [Datahub Price List](https://www.energidataservice.dk/tso-electricity/DatahubPricelist). + +##### Filter Examples + +_N1:_ +| Parameter | Value | +|-----------------|------------| +| chargeTypeCodes | CD,CD R | +| notes | | + +_Nord Energi Net:_ +| Parameter | Value | +|-----------------|------------| +| chargeTypeCodes | TA031U200 | +| notes | Nettarif C | + +#### Hourly Prices + +The format of the `hourlyPrices` JSON array is as follows: + +```json +[ + { + "hourStart": "2023-01-24T15:00:00Z", + "spotPrice": 1.67076001, + "spotPriceCurrency": "DKK", + "netTariff": 0.432225, + "systemTariff": 0.054000, + "electricityTax": 0.008000, + "transmissionNetTariff": 0.058000 + }, + { + "hourStart": "2023-01-24T16:00:00Z", + "spotPrice": 1.859880005, + "spotPriceCurrency": "DKK", + "netTariff": 1.05619, + "systemTariff": 0.054000, + "electricityTax": 0.008000, + "transmissionNetTariff": 0.058000 + } +] +``` + +Future spot prices for the next day are usually available around 13:00 CET and are fetched around that time. +Historic prices older than 12 hours are removed from the JSON array each hour. +Channel configuration for "Include VAT" is ignored, i.e. VAT is excluded. + +## Thing Actions + +Thing actions can be used to perform calculations as well as import prices directly into rules without deserializing JSON from the [hourlyPrices](#hourly-prices) channel. +This is more convenient, much faster, and provides automatic summation of the price elements of interest. + +Actions use cached data for performing operations. +Since data is only fetched when an item is linked to a channel, there might not be any cached data available. +In this case the data will be fetched on demand and cached afterwards. +The first action triggered on a given day may therefore be a bit slower, and is also prone to failing if the server call fails for any reason. +This potential problem can be prevented by linking the indivial channels to items, or by linking the `hourlyPrices` channel to an item. + +### `calculateCheapestPeriod` + +This action will determine the cheapest period for using energy. +It comes in four variants with different input parameters. + +The result is a `Map` with the following keys: + +| Key | Type | Description | +|--------------------|--------------|-------------------------------------------------------| +| CheapestStart | `Instant` | Start time of cheapest calculated period | +| LowestPrice | `BigDecimal` | The total price when starting at cheapest start | +| MostExpensiveStart | `Instant` | Start time of most expensive calculated period | +| HighestPrice | `BigDecimal` | The total price when starting at most expensive start | + +#### `calculateCheapestPeriod` from Duration + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| duration | `Duration` | The duration to fit within the timeslot | + +This is a convenience method that can be used when the power consumption is not known. +The calculation will assume linear consumption and will find the best timeslot based on that. +The this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`. + +Example: + +```javascript +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90)) +``` + +#### `calculateCheapestPeriod` from Duration and Power + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| duration | `Duration` | The duration to fit within the timeslot | +| power | `QuantityType` | Linear power consumption | + +This action is identical the the variant above, but with a known linear power consumption. +As a result the price is also included in the result. + +Example: + +```javascript +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90), 250 | W) +``` + +#### `calculateCheapestPeriod` from Power Phases + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| durationPhases | `List` | List of durations for the phases | +| powerPhases | `List>` | List of power consumption for each corresponding phase | + +This variant is similar to the one above, but is based on a supplied timetable. + +The timetable is supplied as two individual parameters, `durationPhases` and `powerPhases`, which must have the same size. +This can be considered as different phases of using power, so each list member represents a period with a linear use of power. +`durationPhases` should be a List populated by `Duration` objects, while `powerPhases` should be a List populated by `QuantityType` objects for that duration of time. + +Example: + +```javascript +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) +durationPhases.add(Duration.ofMinutes(104)) + +val ArrayList> powerPhases = new ArrayList>() +powerPhases.add(162.162 | W) +powerPhases.add(750 | W) +powerPhases.add(1500 | W) +powerPhases.add(3000 | W) +powerPhases.add(1500 | W) +powerPhases.add(166.666 | W) +powerPhases.add(146.341 | W) +powerPhases.add(0 | W) + +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), durationPhases, powerPhases) +``` + +Please note that the total duration will be calculated automatically as a sum of provided duration phases. +Therefore, if the total duration is longer than the sum of phase durations, the remaining duration must be provided as last item with a corresponding 0 W power item. +This is to ensure that the full program will finish before the provided `latestEnd`. + +#### `calculateCheapestPeriod` from Energy per Phase + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| earliestStart | `Instant` | Earliest start time allowed | +| latestEnd | `Instant` | Latest end time allowed | +| totalDuration | `Duration` | The total duration of all phases | +| durationPhases | `List` | List of durations for the phases | +| energyUsedPerPhase | `QuantityType` | Fixed amount of energy used per phase | + +This variant will assign the provided amount of energy into each phase. +The use case for this variant is a simplification of the previous variant. +For example, a dishwasher may provide energy consumption in 0.1 kWh steps. +In this case it's a simple task to create a timetable accordingly without having to calculate the average power consumption per phase. +Since a last phase may use no significant energy, the total duration must be provided also. + +Example: + +```javascript +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) + +// 0.7 kWh is used in total (number of phases × energy used per phase) +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh) +``` + +### `calculatePrice` + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| start | `Instant` | Start time | +| end | `Instant` | End time | +| power | `QuantityType` | Linear power consumption | + +**Result:** Price as `BigDecimal`. + +This action calculates the price for using given amount of power in the period from `start` till `end`. + +Example: + +```javascript +var price = actions.calculatePrice(now.toInstant(), now.plusHours(4).toInstant, 200 | W) +``` + +### `getPrices` + +| Parameter | Type | Description | +|--------------------|-----------------------------|--------------------------------------------------------| +| priceElements | `String` | Comma-separated list of price elements to include | + +**Result:** `Map` + +The parameter `priceElements` is a case-insensitive comma-separated list of price elements to include in the returned hourly prices. +These elements can be requested: + +| Price element | Description | +|-----------------------|-------------------------| +| SpotPrice | Spot price | +| NetTariff | Net tariff | +| SystemTariff | System tariff | +| ElectricityTax | Electricity tax | +| TransmissionNetTariff | Transmission net tariff | + +Using `null` as parameter returns the total prices including all price elements. + +Example: + +```javascript +var priceMap = actions.getPrices("SpotPrice,NetTariff"); +``` + +## Full Example + +### Thing Configuration + +```java +Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] { + Channels: + Number : electricity#currentSpotPrice [ includeVAT="true" ] + Number : electricity#currentNetTariff [ includeVAT="true" ] + Number : electricity#currentSystemTariff [ includeVAT="true" ] + Number : electricity#currentElectricityTax [ includeVAT="true" ] + Number : electricity#currentTransmissionNetTariff [ includeVAT="true" ] +} +``` + +### Item Configuration + +```java +Group:Number:SUM CurrentTotalPrice "Current Total Price" +Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSpotPrice"} +Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentNetTariff"} +Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSystemTariff"} +Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentElectricityTax"} +Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentTransmissionNetTariff"} +String HourlyPrices "Hourly Prices" {channel="energidataservice:service:energidataservice:electricity#hourlyPrices"} +``` + +### Thing Actions Example + +```javascript +import java.time.Duration +import java.util.ArrayList +import java.util.Map +import java.time.temporal.ChronoUnit + +val actions = getActions("energidataservice", "energidataservice:service:energidataservice"); + +var priceMap = actions.getPrices(null); +var hourStart = now.toInstant().truncatedTo(ChronoUnit.HOURS) +logInfo("Current total price excl. VAT", priceMap.get(hourStart).toString) + +var priceMap = actions.getPrices("SpotPrice,NetTariff"); +logInfo("Current spot price + net tariff excl. VAT", priceMap.get(hourStart).toString) + +var price = actions.calculatePrice(Instant.now, now.plusHours(1).toInstant, 150 | W) +logInfo("Total price for using 150 W for the next hour", price.toString) + +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) +durationPhases.add(Duration.ofMinutes(104)) + +val ArrayList> consumptionPhases = new ArrayList>() +consumptionPhases.add(162.162 | W) +consumptionPhases.add(750 | W) +consumptionPhases.add(1500 | W) +consumptionPhases.add(3000 | W) +consumptionPhases.add(1500 | W) +consumptionPhases.add(166.666 | W) +consumptionPhases.add(146.341 | W) +consumptionPhases.add(0 | W) + +var Map result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases) +logInfo("Lowest price", result.get("LowestPrice")) +logInfo("Cheapest start", result.get("CheapestStart")) +logInfo("Highest price price", result.get("HighestPrice")) +logInfo("Most expensive start", result.get("MostExpensiveStart")) + +// This is a simpler version taking advantage of the fact that each interval here represents 0.1 kWh of consumed energy. +// In this example we have to provide the total duration to make sure we fit the latest end. This is because there is no +// registered consumption in the last phase. +val ArrayList durationPhases = new ArrayList() +durationPhases.add(Duration.ofMinutes(37)) +durationPhases.add(Duration.ofMinutes(8)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(2)) +durationPhases.add(Duration.ofMinutes(4)) +durationPhases.add(Duration.ofMinutes(36)) +durationPhases.add(Duration.ofMinutes(41)) +durationPhases.add(Duration.ofMinutes(104)) + +var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh) + +``` diff --git a/bundles/org.openhab.binding.energidataservice/pom.xml b/bundles/org.openhab.binding.energidataservice/pom.xml new file mode 100644 index 0000000000000..4e4d39fddfb21 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.energidataservice + + openHAB Add-ons :: Bundles :: Energi Data Service Binding + + + + com.google.code.gson + gson + 2.10.1 + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml new file mode 100644 index 0000000000000..69ed1af643ffc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.energidataservice/${project.version} + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java new file mode 100644 index 0000000000000..c55282dd94efb --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Currency; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.energidataservice.internal.api.ChargeType; +import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; +import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords; +import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer; +import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; +import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.core.i18n.TimeZoneProvider; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link ApiController} is responsible for interacting with Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ApiController { + private static final String ENDPOINT = "https://api.energidataservice.dk/"; + private static final String DATASET_PATH = "dataset/"; + + private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices"; + private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist"; + + private static final String FILTER_KEY_PRICE_AREA = "PriceArea"; + private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType"; + private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode"; + private static final String FILTER_KEY_GLN_NUMBER = "GLN_Number"; + private static final String FILTER_KEY_NOTE = "Note"; + + private static final String HEADER_REMAINING_CALLS = "RemainingCalls"; + private static final String HEADER_TOTAL_CALLS = "TotalCalls"; + + private final Logger logger = LoggerFactory.getLogger(ApiController.class); + private final Gson gson = new GsonBuilder() // + .registerTypeAdapter(Instant.class, new InstantDeserializer()) // + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) // + .create(); + private final HttpClient httpClient; + private final TimeZoneProvider timeZoneProvider; + private final String userAgent; + + public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) { + this.httpClient = httpClient; + this.timeZoneProvider = timeZoneProvider; + userAgent = "openHAB " + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); + } + + /** + * Retrieve spot prices for requested area and in requested {@link Currency}. + * + * @param priceArea Usually DK1 or DK2 + * @param currency DKK or EUR + * @param start Specifies the start point of the period for the data request + * @param properties Map of properties which will be updated with metadata from headers + * @return Records with pairs of hour start and price in requested currency. + * @throws InterruptedException + * @throws DataServiceException + */ + public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start, + Map properties) throws InterruptedException, DataServiceException { + if (!SUPPORTED_CURRENCIES.contains(currency)) { + throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); + } + + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES) + .param("start", start.toString()) // + .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") // + .param("columns", "HourUTC,SpotPrice" + currency) // + .agent(userAgent) // + .method(HttpMethod.GET); + + logger.trace("GET request for {}", request.getURI()); + + try { + ContentResponse response = request.send(); + + updatePropertiesFromResponse(response, properties); + + int status = response.getStatus(); + if (!HttpStatus.isSuccess(status)) { + throw new DataServiceException("The request failed with HTTP error " + status, status); + } + String responseContent = response.getContentAsString(); + if (responseContent.isEmpty()) { + throw new DataServiceException("Empty response"); + } + logger.trace("Response content: '{}'", responseContent); + + ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); + if (records == null) { + throw new DataServiceException("Error parsing response"); + } + + if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) { + throw new DataServiceException("No records"); + } + + return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(ElspotpriceRecord[]::new); + } catch (JsonSyntaxException e) { + throw new DataServiceException("Error parsing response", e); + } catch (TimeoutException | ExecutionException e) { + throw new DataServiceException(e); + } + } + + private void updatePropertiesFromResponse(ContentResponse response, Map properties) { + HttpFields headers = response.getHeaders(); + String remainingCalls = headers.get(HEADER_REMAINING_CALLS); + if (remainingCalls != null) { + properties.put(PROPERTY_REMAINING_CALLS, remainingCalls); + } + String totalCalls = headers.get(HEADER_TOTAL_CALLS); + if (totalCalls != null) { + properties.put(PROPERTY_TOTAL_CALLS, totalCalls); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT); + properties.put(PROPERTY_LAST_CALL, LocalDateTime.now(timeZoneProvider.getTimeZone()).format(formatter)); + } + + /** + * Retrieve datahub pricelists for requested GLN and charge type/charge type code. + * + * @param globalLocationNumber Global Location Number of the Charge Owner + * @param chargeType Charge type (Subscription, Fee or Tariff). + * @param tariffFilter Tariff filter (charge type codes and notes). + * @param properties Map of properties which will be updated with metadata from headers + * @return Price list for requested GLN and note. + * @throws InterruptedException + * @throws DataServiceException + */ + public Collection getDatahubPriceLists(GlobalLocationNumber globalLocationNumber, + ChargeType chargeType, DatahubTariffFilter tariffFilter, Map properties) + throws InterruptedException, DataServiceException { + String columns = "ValidFrom,ValidTo,ChargeTypeCode"; + for (int i = 1; i < 25; i++) { + columns += ",Price" + i; + } + + Map> filterMap = new HashMap<>(Map.of( // + FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), // + FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString()))); + + Collection chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings(); + if (!chargeTypeCodes.isEmpty()) { + filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes); + } + + Collection notes = tariffFilter.getNotes(); + if (!notes.isEmpty()) { + filterMap.put(FILTER_KEY_NOTE, notes); + } + + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST) + .param("filter", mapToFilter(filterMap)) // + .param("columns", columns) // + .agent(userAgent) // + .method(HttpMethod.GET); + + DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter(); + if (!dateQueryParameter.isEmpty()) { + request = request.param("start", dateQueryParameter.toString()); + } + + logger.trace("GET request for {}", request.getURI()); + + try { + ContentResponse response = request.send(); + + updatePropertiesFromResponse(response, properties); + + int status = response.getStatus(); + if (!HttpStatus.isSuccess(status)) { + throw new DataServiceException("The request failed with HTTP error " + status, status); + } + String responseContent = response.getContentAsString(); + if (responseContent.isEmpty()) { + throw new DataServiceException("Empty response"); + } + logger.trace("Response content: '{}'", responseContent); + + DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class); + if (records == null) { + throw new DataServiceException("Error parsing response"); + } + + if (records.limit() > 0 && records.limit() < records.total()) { + logger.warn("{} price list records available, but only {} returned.", records.total(), records.limit()); + } + + if (Objects.isNull(records.records())) { + return List.of(); + } + + return Arrays.stream(records.records()).filter(Objects::nonNull).toList(); + } catch (JsonSyntaxException e) { + throw new DataServiceException("Error parsing response", e); + } catch (TimeoutException | ExecutionException e) { + throw new DataServiceException(e); + } + } + + private String mapToFilter(Map> map) { + return "{" + map.entrySet().stream().map( + e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]") + .collect(Collectors.joining(",")) + "}"; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java new file mode 100644 index 0000000000000..d4673e1afa62e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/CacheManager.java @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; + +/** + * The {@link CacheManager} is responsible for maintaining a cache of received + * data from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class CacheManager { + + public final static int NUMBER_OF_HISTORIC_HOURS = 12; + public final static int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS; + public final static int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS; + + private final Clock clock; + private final PriceListParser priceListParser = new PriceListParser(); + + private Collection netTariffRecords = new ArrayList<>(); + private Collection systemTariffRecords = new ArrayList<>(); + private Collection electricityTaxRecords = new ArrayList<>(); + private Collection transmissionNetTariffRecords = new ArrayList<>(); + + private Map spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE); + private Map netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + private Map transmissionNetTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE); + + public CacheManager() { + this(Clock.systemDefaultZone()); + } + + public CacheManager(Clock clock) { + this.clock = clock.withZone(NORD_POOL_TIMEZONE); + } + + /** + * Clear all cached data. + */ + public void clear() { + netTariffRecords.clear(); + systemTariffRecords.clear(); + electricityTaxRecords.clear(); + transmissionNetTariffRecords.clear(); + + spotPriceMap.clear(); + netTariffMap.clear(); + systemTariffMap.clear(); + electricityTaxMap.clear(); + transmissionNetTariffMap.clear(); + } + + /** + * Convert and cache the supplied {@link ElspotpriceRecord}s. + * + * @param records The records as received from Energi Data Service. + * @param currency The currency in which the records were requested. + */ + public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) { + boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency); + for (ElspotpriceRecord record : records) { + spotPriceMap.put(record.hour(), + (isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000))); + } + cleanup(); + } + + /** + * Replace current "raw"/unprocessed net tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putNetTariffs(Collection records) { + putDatahubRecords(netTariffRecords, records); + updateNetTariffs(); + } + + /** + * Replace current "raw"/unprocessed system tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putSystemTariffs(Collection records) { + putDatahubRecords(systemTariffRecords, records); + updateSystemTariffs(); + } + + /** + * Replace current "raw"/unprocessed electricity tax records in cache. + * Map of hourly taxes will be updated automatically. + * + * @param records to cache + */ + public void putElectricityTaxes(Collection records) { + putDatahubRecords(electricityTaxRecords, records); + updateElectricityTaxes(); + } + + /** + * Replace current "raw"/unprocessed transmission net tariff records in cache. + * Map of hourly tariffs will be updated automatically. + * + * @param records to cache + */ + public void putTransmissionNetTariffs(Collection records) { + putDatahubRecords(transmissionNetTariffRecords, records); + updateTransmissionNetTariffs(); + } + + private void putDatahubRecords(Collection destination, + Collection source) { + LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE)) + .minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS); + + destination.clear(); + destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList()); + } + + /** + * Update map of hourly net tariffs from internal cache. + */ + public void updateNetTariffs() { + netTariffMap = priceListParser.toHourly(netTariffRecords); + cleanup(); + } + + /** + * Update map of system tariffs from internal cache. + */ + public void updateSystemTariffs() { + systemTariffMap = priceListParser.toHourly(systemTariffRecords); + cleanup(); + } + + /** + * Update map of electricity taxes from internal cache. + */ + public void updateElectricityTaxes() { + electricityTaxMap = priceListParser.toHourly(electricityTaxRecords); + cleanup(); + } + + /** + * Update map of hourly transmission net tariffs from internal cache. + */ + public void updateTransmissionNetTariffs() { + transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords); + cleanup(); + } + + /** + * Get current spot price. + * + * @return spot price currently valid + */ + public @Nullable BigDecimal getSpotPrice() { + return getSpotPrice(Instant.now(clock)); + } + + /** + * Get spot price valid at provided instant. + * + * @param time {@link Instant} for which to get the spot price + * @return spot price at given time or null if not available + */ + public @Nullable BigDecimal getSpotPrice(Instant time) { + return spotPriceMap.get(getHourStart(time)); + } + + /** + * Get map of all cached spot prices. + * + * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getSpotPrices() { + return new HashMap(spotPriceMap); + } + + /** + * Get current net tariff. + * + * @return net tariff currently valid + */ + public @Nullable BigDecimal getNetTariff() { + return getNetTariff(Instant.now(clock)); + } + + /** + * Get net tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the net tariff + * @return net tariff at given time or null if not available + */ + public @Nullable BigDecimal getNetTariff(Instant time) { + return netTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached net tariffs. + * + * @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getNetTariffs() { + return new HashMap(netTariffMap); + } + + /** + * Get current system tariff. + * + * @return system tariff currently valid + */ + public @Nullable BigDecimal getSystemTariff() { + return getSystemTariff(Instant.now(clock)); + } + + /** + * Get system tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the system tariff + * @return system tariff at given time or null if not available + */ + public @Nullable BigDecimal getSystemTariff(Instant time) { + return systemTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached system tariffs. + * + * @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getSystemTariffs() { + return new HashMap(systemTariffMap); + } + + /** + * Get current electricity tax. + * + * @return electricity tax currently valid + */ + public @Nullable BigDecimal getElectricityTax() { + return getElectricityTax(Instant.now(clock)); + } + + /** + * Get electricity tax valid at provided instant. + * + * @param time {@link Instant} for which to get the electricity tax + * @return electricity tax at given time or null if not available + */ + public @Nullable BigDecimal getElectricityTax(Instant time) { + return electricityTaxMap.get(getHourStart(time)); + } + + /** + * Get map of all cached electricity taxes. + * + * @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getElectricityTaxes() { + return new HashMap(electricityTaxMap); + } + + /** + * Get current transmission net tariff. + * + * @return transmission net tariff currently valid + */ + public @Nullable BigDecimal getTransmissionNetTariff() { + return getTransmissionNetTariff(Instant.now(clock)); + } + + /** + * Get transmission net tariff valid at provided instant. + * + * @param time {@link Instant} for which to get the transmission net tariff + * @return transmission net tariff at given time or null if not available + */ + public @Nullable BigDecimal getTransmissionNetTariff(Instant time) { + return transmissionNetTariffMap.get(getHourStart(time)); + } + + /** + * Get map of all cached transmission net tariffs. + * + * @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back + */ + public Map getTransmissionNetTariffs() { + return new HashMap(transmissionNetTariffMap); + } + + /** + * Get number of future spot prices including current hour. + * + * @return number of future spot prices + */ + public long getNumberOfFutureSpotPrices() { + Instant currentHourStart = getCurrentHourStart(); + + return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count(); + } + + /** + * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached. + * + * @return true if historic spot prices are cached + */ + public boolean areHistoricSpotPricesCached() { + return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS)); + } + + /** + * Check if all current spot prices are cached taking into consideration that next day's spot prices + * should be available at 13:00 CET. + * + * @return true if spot prices are fully cached + */ + public boolean areSpotPricesFullyCached() { + Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant(); + LocalTime now = LocalTime.now(clock); + if (now.isAfter(DAILY_REFRESH_TIME_CET)) { + end = end.plus(24, ChronoUnit.HOURS); + } + + return arePricesCached(spotPriceMap, end); + } + + private boolean arePricesCached(Map priceMap, Instant end) { + for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1, + ChronoUnit.HOURS)) { + if (priceMap.get(hourStart) == null) { + return false; + } + } + + return true; + } + + /** + * Check if we have "raw" net tariff records cached which are valid tomorrow. + * + * @return true if net tariff records for tomorrow are cached + */ + public boolean areNetTariffsValidTomorrow() { + return isValidNextDay(netTariffRecords); + } + + /** + * Check if we have "raw" system tariff records cached which are valid tomorrow. + * + * @return true if system tariff records for tomorrow are cached + */ + public boolean areSystemTariffsValidTomorrow() { + return isValidNextDay(systemTariffRecords); + } + + /** + * Check if we have "raw" electricity tax records cached which are valid tomorrow. + * + * @return true if electricity tax records for tomorrow are cached + */ + public boolean areElectricityTaxesValidTomorrow() { + return isValidNextDay(electricityTaxRecords); + } + + /** + * Check if we have "raw" transmission net tariff records cached which are valid tomorrow. + * + * @return true if transmission net tariff records for tomorrow are cached + */ + public boolean areTransmissionNetTariffsValidTomorrow() { + return isValidNextDay(transmissionNetTariffRecords); + } + + /** + * Remove historic prices. + */ + public void cleanup() { + Instant firstHourStart = getFirstHourStart(); + + spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + netTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + systemTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + electricityTaxMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + transmissionNetTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart)); + } + + private boolean isValidNextDay(Collection records) { + LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE) + .truncatedTo(ChronoUnit.HOURS); + LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS); + + return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight)); + } + + private Instant getCurrentHourStart() { + return getHourStart(Instant.now(clock)); + } + + private Instant getFirstHourStart() { + return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)); + } + + private Instant getHourStart(Instant instant) { + return instant.truncatedTo(ChronoUnit.HOURS); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java new file mode 100644 index 0000000000000..38358f0c03343 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Currency; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EnergiDataServiceBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class EnergiDataServiceBindingConstants { + + private static final String BINDING_ID = "energidataservice"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service"); + + // List of all Channel Group ids + public static final String CHANNEL_GROUP_ELECTRICITY = "electricity"; + + // List of all Channel ids + public static final String CHANNEL_CURRENT_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSpotPrice"; + public static final String CHANNEL_CURRENT_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentNetTariff"; + public static final String CHANNEL_CURRENT_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSystemTariff"; + public static final String CHANNEL_CURRENT_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentElectricityTax"; + public static final String CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentTransmissionNetTariff"; + public static final String CHANNEL_HOURLY_PRICES = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR + + "hourlyPrices"; + + public static final Set ELECTRICITY_CHANNELS = Set.of(CHANNEL_CURRENT_SPOT_PRICE, + CHANNEL_CURRENT_NET_TARIFF, CHANNEL_CURRENT_SYSTEM_TARIFF, CHANNEL_CURRENT_ELECTRICITY_TAX, + CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, CHANNEL_HOURLY_PRICES); + + // List of all properties + public static final String PROPERTY_REMAINING_CALLS = "remainingCalls"; + public static final String PROPERTY_TOTAL_CALLS = "totalCalls"; + public static final String PROPERTY_LAST_CALL = "lastCall"; + public static final String PROPERTY_NEXT_CALL = "nextCall"; + + // List of supported currencies + public static final Currency CURRENCY_DKK = Currency.getInstance("DKK"); + public static final Currency CURRENCY_EUR = Currency.getInstance("EUR"); + + public static final Set SUPPORTED_CURRENCIES = Set.of(CURRENCY_DKK, CURRENCY_EUR); + + // Time-zone of Datahub + public static final ZoneId DATAHUB_TIMEZONE = ZoneId.of("CET"); + public static final ZoneId NORD_POOL_TIMEZONE = ZoneId.of("CET"); + + // Other + public static final LocalTime DAILY_REFRESH_TIME_CET = LocalTime.of(13, 0); + public static final LocalDate ENERGINET_CUTOFF_DATE = LocalDate.of(2023, 1, 1); + public static final String PROPERTY_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java new file mode 100644 index 0000000000000..3524f90b1ca9f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceCalculator.java @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.energidataservice.internal.exception.MissingPriceException; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides calculations based on price maps. + * This is the current stage of evolution. + * Ideally this binding would simply provide data in a well-defined format for + * openHAB core. Operations on this data could then be implemented in core. + * This way there would be a unified interface from rules, and the calculations + * could be reused between different data providers (bindings). + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class PriceCalculator { + + private final Logger logger = LoggerFactory.getLogger(PriceCalculator.class); + + private final Map priceMap; + + public PriceCalculator(Map priceMap) { + this.priceMap = priceMap; + } + + /** + * Calculate cheapest period from list of durations with specified amount of energy + * used per phase. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param totalDuration Total duration to fit. + * @param durationPhases List of {@link Duration}'s representing different phases of using power. + * @param energyUsedPerPhase Amount of energy used per phase. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration, + List durationPhases, QuantityType energyUsedPerPhase) throws MissingPriceException { + QuantityType energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR); + if (energyInWattHour == null) { + throw new IllegalArgumentException( + "Invalid unit " + energyUsedPerPhase.getUnit() + ", expected energy unit"); + } + // watts = (kWh × 1,000) ÷ hrs + int numerator = energyInWattHour.intValue() * 3600; + List> consumptionPhases = new ArrayList<>(); + Duration remainingDuration = totalDuration; + for (Duration phase : durationPhases) { + consumptionPhases.add(QuantityType.valueOf(numerator / phase.getSeconds(), Units.WATT)); + remainingDuration = remainingDuration.minus(phase); + } + if (remainingDuration.isNegative()) { + throw new IllegalArgumentException("totalDuration must be equal to or greater than sum of phases"); + } + if (!remainingDuration.isZero()) { + List durationsWithTermination = new ArrayList<>(durationPhases); + durationsWithTermination.add(remainingDuration); + consumptionPhases.add(QuantityType.valueOf(0, Units.WATT)); + return calculateCheapestPeriod(earliestStart, latestEnd, durationsWithTermination, consumptionPhases); + } + return calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, consumptionPhases); + } + + /** + * Calculate cheapest period from duration with linear power usage. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param duration Duration to fit. + * @param power Power consumption for the duration of time. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration, + QuantityType power) throws MissingPriceException { + return calculateCheapestPeriod(earliestStart, latestEnd, List.of(duration), List.of(power)); + } + + /** + * Calculate cheapest period from list of durations with corresponding list of consumption + * per duration. + * + * @param earliestStart Earliest allowed start time. + * @param latestEnd Latest allowed end time. + * @param durationPhases List of {@link Duration}'s representing different phases of using power. + * @param consumptionPhases Corresponding List of power consumption for the duration of time. + * + * @return Map containing resulting values + */ + public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, + List durationPhases, List> consumptionPhases) throws MissingPriceException { + if (durationPhases.size() != consumptionPhases.size()) { + throw new IllegalArgumentException("Number of phases do not match"); + } + Map result = new HashMap<>(); + Duration totalDuration = durationPhases.stream().reduce(Duration.ZERO, Duration::plus); + Instant calculationStart = earliestStart; + Instant calculationEnd = earliestStart.plus(totalDuration); + BigDecimal lowestPrice = BigDecimal.valueOf(Double.MAX_VALUE); + BigDecimal highestPrice = BigDecimal.ZERO; + Instant cheapestStart = Instant.MIN; + Instant mostExpensiveStart = Instant.MIN; + + while (calculationEnd.compareTo(latestEnd) <= 0) { + BigDecimal currentPrice = BigDecimal.ZERO; + Duration minDurationUntilNextHour = Duration.ofHours(1); + Instant atomStart = calculationStart; + + int i = 0; + for (Duration atomDuration : durationPhases) { + Instant atomEnd = atomStart.plus(atomDuration); + Instant hourStart = atomStart.truncatedTo(ChronoUnit.HOURS); + Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS); + + // Get next intersection with hourly rate change. + Duration durationUntilNextHour = Duration.between(atomStart, hourEnd); + if (durationUntilNextHour.compareTo(minDurationUntilNextHour) < 0) { + minDurationUntilNextHour = durationUntilNextHour; + } + + BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, consumptionPhases.get(i)); + currentPrice = currentPrice.add(atomPrice); + atomStart = atomEnd; + + i++; + } + + if (currentPrice.compareTo(lowestPrice) < 0) { + lowestPrice = currentPrice; + cheapestStart = calculationStart; + } + if (currentPrice.compareTo(highestPrice) > 0) { + highestPrice = currentPrice; + mostExpensiveStart = calculationStart; + } + + // Now fast forward to next hourly rate intersection. + calculationStart = calculationStart.plus(minDurationUntilNextHour); + calculationEnd = calculationStart.plus(totalDuration); + } + + if (!cheapestStart.equals(Instant.MIN)) { + result.put("CheapestStart", cheapestStart); + result.put("LowestPrice", lowestPrice); + result.put("MostExpensiveStart", mostExpensiveStart); + result.put("HighestPrice", highestPrice); + } + + return result; + } + + /** + * Calculate total price from 'start' to 'end' given linear power consumption. + * + * @param start Start time + * @param end End time + * @param power The current power consumption. + */ + public BigDecimal calculatePrice(Instant start, Instant end, QuantityType power) + throws MissingPriceException { + QuantityType quantityInWatt = power.toUnit(Units.WATT); + if (quantityInWatt == null) { + throw new IllegalArgumentException("Invalid unit " + power.getUnit() + ", expected power unit"); + } + BigDecimal watt = new BigDecimal(quantityInWatt.intValue()); + if (watt.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + + Instant current = start; + BigDecimal result = BigDecimal.ZERO; + while (current.isBefore(end)) { + Instant hourStart = current.truncatedTo(ChronoUnit.HOURS); + Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS); + + BigDecimal currentPrice = priceMap.get(hourStart); + if (currentPrice == null) { + throw new MissingPriceException("Price missing at " + hourStart.toString()); + } + + Instant currentStart = hourStart; + if (start.isAfter(hourStart)) { + currentStart = start; + } + Instant currentEnd = hourEnd; + if (end.isBefore(hourEnd)) { + currentEnd = end; + } + + // E(kWh) = P(W) × t(hr) / 1000 + Duration duration = Duration.between(currentStart, currentEnd); + BigDecimal contribution = currentPrice.multiply(watt).multiply( + new BigDecimal(duration.getSeconds()).divide(new BigDecimal(3600000), 9, RoundingMode.HALF_UP)); + result = result.add(contribution); + logger.trace("Period {}-{}: {} @ {}", currentStart, currentEnd, contribution, currentPrice); + + current = hourEnd; + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java new file mode 100644 index 0000000000000..5f4bedc1a3f31 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/PriceListParser.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; + +/** + * Parses results from {@link DatahubPricelistRecords} into map of hourly tariffs. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class PriceListParser { + + private final Clock clock; + + public PriceListParser() { + this(Clock.system(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + } + + public PriceListParser(Clock clock) { + this.clock = clock; + } + + public Map toHourly(Collection records) { + Map totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); + records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> { + Map currentMap = toHourly(records, chargeTypeCode); + for (Entry current : currentMap.entrySet()) { + BigDecimal total = totalMap.get(current.getKey()); + if (total == null) { + total = BigDecimal.ZERO; + } + totalMap.put(current.getKey(), total.add(current.getValue())); + } + }); + + return totalMap; + } + + public Map toHourly(Collection records, String chargeTypeCode) { + Map tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE); + + Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS) + .truncatedTo(ChronoUnit.HOURS); + Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS) + .truncatedTo(ChronoUnit.DAYS); + + LocalDateTime previousValidFrom = LocalDateTime.MAX; + LocalDateTime previousValidTo = LocalDateTime.MIN; + Map tariffs = Map.of(); + for (Instant hourStart = firstHourStart; hourStart + .isBefore(lastHourStart); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) { + LocalDateTime localDateTime = hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE) + .toLocalDateTime(); + if (localDateTime.compareTo(previousValidFrom) < 0 || localDateTime.compareTo(previousValidTo) >= 0) { + DatahubPricelistRecord priceList = getTariffs(records, localDateTime, chargeTypeCode); + if (priceList != null) { + tariffs = priceList.getTariffMap(); + previousValidFrom = priceList.validFrom(); + previousValidTo = priceList.validTo(); + } else { + tariffs = Map.of(); + } + } + + LocalTime localTime = LocalTime + .of(hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE).getHour(), 0); + BigDecimal tariff = tariffs.get(localTime); + if (tariff != null) { + tariffMap.put(hourStart, tariff); + } + } + + return tariffMap; + } + + private @Nullable DatahubPricelistRecord getTariffs(Collection records, + LocalDateTime localDateTime, String chargeTypeCode) { + return records.stream() + .filter(record -> localDateTime.compareTo(record.validFrom()) >= 0 + && localDateTime.compareTo(record.validTo()) < 0 + && record.chargeTypeCode().equals(chargeTypeCode)) + .findFirst().orElse(null); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java new file mode 100644 index 0000000000000..a39897deb7137 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActions.java @@ -0,0 +1,381 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.action; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.PriceCalculator; +import org.openhab.binding.energidataservice.internal.exception.MissingPriceException; +import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.ActionOutput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context. + * + * @author Jacob Laursen - Initial contribution + */ +@ThingActionsScope(name = "energidataservice") +@NonNullByDefault +public class EnergiDataServiceActions implements ThingActions { + + private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class); + + private @Nullable EnergiDataServiceHandler handler; + + private enum PriceElement { + SPOT_PRICE("spotprice"), + NET_TARIFF("nettariff"), + SYSTEM_TARIFF("systemtariff"), + ELECTRICITY_TAX("electricitytax"), + TRANSMISSION_NET_TARIFF("transmissionnettariff"); + + private static final Map NAME_MAP = Stream.of(values()) + .collect(Collectors.toMap(PriceElement::toString, Function.identity())); + + private String name; + + private PriceElement(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static PriceElement fromString(final String name) { + PriceElement myEnum = NAME_MAP.get(name.toLowerCase()); + if (null == myEnum) { + throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s", + name, Arrays.asList(values()))); + } + return myEnum; + } + } + + @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description") + public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices() { + return getPrices(Arrays.stream(PriceElement.values()).collect(Collectors.toSet())); + } + + @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description") + public @ActionOutput(name = "prices", type = "java.util.Map") Map getPrices( + @ActionInput(name = "priceElements", label = "@text/action.get-prices.priceElements.label", description = "@text/action.get-prices.priceElements.description") @Nullable String priceElements) { + if (priceElements == null) { + logger.warn("Argument 'priceElements' is null"); + return Map.of(); + } + + Set priceElementsSet; + try { + priceElementsSet = new HashSet( + Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList()); + } catch (IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + + return getPrices(priceElementsSet); + } + + @RuleAction(label = "@text/action.calculate-price.label", description = "@text/action.calculate-price.description") + public @ActionOutput(name = "price", type = "java.math.BigDecimal") BigDecimal calculatePrice( + @ActionInput(name = "start", type = "java.time.Instant") Instant start, + @ActionInput(name = "end", type = "java.time.Instant") Instant end, + @ActionInput(name = "power", type = "QuantityType") QuantityType power) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculatePrice(start, end, power); + } catch (MissingPriceException e) { + logger.warn("{}", e.getMessage()); + return BigDecimal.ZERO; + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "duration", type = "java.time.Duration") Duration duration) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + Map intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, + duration, QuantityType.valueOf(1000, Units.WATT)); + + // Create new result with stripped price information. + Map result = new HashMap<>(); + Object value = intermediateResult.get("CheapestStart"); + if (value != null) { + result.put("CheapestStart", value); + } + value = intermediateResult.get("MostExpensiveStart"); + if (value != null) { + result.put("MostExpensiveStart", value); + } + return result; + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "duration", type = "java.time.Duration") Duration duration, + @ActionInput(name = "power", type = "QuantityType") QuantityType power) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "totalDuration", type = "java.time.Duration") Duration totalDuration, + @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases, + @ActionInput(name = "energyUsedPerPhase", type = "QuantityType") QuantityType energyUsedPerPhase) { + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases, + energyUsedPerPhase); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description") + public @ActionOutput(name = "result", type = "java.util.Map") Map calculateCheapestPeriod( + @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart, + @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd, + @ActionInput(name = "durationPhases", type = "java.util.List") List durationPhases, + @ActionInput(name = "powerPhases", type = "java.util.List>") List> powerPhases) { + if (durationPhases.size() != powerPhases.size()) { + logger.warn("Number of duration phases ({}) is different from number of consumption phases ({})", + durationPhases.size(), powerPhases.size()); + return Map.of(); + } + PriceCalculator priceCalculator = new PriceCalculator(getPrices()); + + try { + return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases); + } catch (MissingPriceException | IllegalArgumentException e) { + logger.warn("{}", e.getMessage()); + return Map.of(); + } + } + + private Map getPrices(Set priceElements) { + EnergiDataServiceHandler handler = this.handler; + if (handler == null) { + logger.warn("EnergiDataServiceActions ThingHandler is null."); + return Map.of(); + } + + Map prices; + boolean spotPricesRequired; + if (priceElements.contains(PriceElement.SPOT_PRICE)) { + if (priceElements.size() > 1 && !handler.getCurrency().equals(CURRENCY_DKK)) { + logger.warn("Cannot calculate sum when spot price currency is {}", handler.getCurrency()); + return Map.of(); + } + prices = handler.getSpotPrices(); + spotPricesRequired = true; + } else { + spotPricesRequired = false; + prices = new HashMap<>(); + } + + if (priceElements.contains(PriceElement.NET_TARIFF)) { + Map netTariffMap = handler.getNetTariffs(); + mergeMaps(prices, netTariffMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) { + Map systemTariffMap = handler.getSystemTariffs(); + mergeMaps(prices, systemTariffMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) { + Map electricityTaxMap = handler.getElectricityTaxes(); + mergeMaps(prices, electricityTaxMap, !spotPricesRequired); + } + + if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) { + Map transmissionNetTariffMap = handler.getTransmissionNetTariffs(); + mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired); + } + + return prices; + } + + private void mergeMaps(Map destinationMap, Map sourceMap, + boolean createNew) { + for (Entry source : sourceMap.entrySet()) { + Instant key = source.getKey(); + BigDecimal sourceValue = source.getValue(); + BigDecimal destinationValue = destinationMap.get(key); + if (destinationValue != null) { + destinationMap.put(key, sourceValue.add(destinationValue)); + } else if (createNew) { + destinationMap.put(key, sourceValue); + } + } + } + + /** + * Static get prices method for DSL rule compatibility. + * + * @param actions + * @param priceElements Comma-separated list of price elements to include in prices. + * @return Map of prices + */ + public static Map getPrices(@Nullable ThingActions actions, @Nullable String priceElements) { + if (actions instanceof EnergiDataServiceActions) { + if (priceElements != null && !priceElements.isBlank()) { + return ((EnergiDataServiceActions) actions).getPrices(priceElements); + } else { + return ((EnergiDataServiceActions) actions).getPrices(); + } + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + /** + * Static get prices method for DSL rule compatibility. + * + * @param actions + * @param start Start time + * @param end End time + * @param power Constant power consumption + * @return Map of prices + */ + public static BigDecimal calculatePrice(@Nullable ThingActions actions, @Nullable Instant start, + @Nullable Instant end, @Nullable QuantityType power) { + if (start == null || end == null || power == null) { + return BigDecimal.ZERO; + } + if (actions instanceof EnergiDataServiceActions) { + return ((EnergiDataServiceActions) actions).calculatePrice(start, end, power); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || duration == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration, + @Nullable QuantityType power) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || duration == null || power == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration, + power); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration, + @Nullable List durationPhases, @Nullable QuantityType energyUsedPerPhase) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || totalDuration == null || durationPhases == null + || energyUsedPerPhase == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, + durationPhases, energyUsedPerPhase); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + public static Map calculateCheapestPeriod(@Nullable ThingActions actions, + @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List durationPhases, + @Nullable List> powerPhases) { + if (actions instanceof EnergiDataServiceActions) { + if (earliestStart == null || latestEnd == null || durationPhases == null || powerPhases == null) { + return Map.of(); + } + return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, + durationPhases, powerPhases); + } else { + throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class."); + } + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof EnergiDataServiceHandler) { + this.handler = (EnergiDataServiceHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java new file mode 100644 index 0000000000000..19d242b740915 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeType.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Charge type for DatahubPricelist dataset. + * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}} + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum ChargeType { + Subscription("D01"), + Fee("D02"), + Tariff("D03"); + + private final String code; + + ChargeType(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java new file mode 100644 index 0000000000000..b9228a17215bc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/ChargeTypeCode.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Charge type code for DatahubPricelist dataset. + * See {@link https://www.energidataservice.dk/tso-electricity/DatahubPricelist#metadata-info}} + * These codes are defined by the individual grid companies. + * For example, N1 uses "CD" for "Nettarif C" and "CD R" for "Rabat på nettarif N1 A/S". + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ChargeTypeCode { + + private static final int MAX_LENGTH = 20; + + private final String code; + + public ChargeTypeCode(String code) { + if (code.length() > MAX_LENGTH) { + throw new IllegalArgumentException("Maximum length exceeded: " + code); + } + this.code = code; + } + + @Override + public String toString() { + return code; + } + + public static ChargeTypeCode of(String code) { + return new ChargeTypeCode(code); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java new file mode 100644 index 0000000000000..ecbec4a8bb94c --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilter.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Filter for the {@link DatahubPricelist} dataset. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DatahubTariffFilter { + + private final Set chargeTypeCodes; + private final Set notes; + private final DateQueryParameter dateQueryParameter; + + public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) { + this(filter.chargeTypeCodes, filter.notes, dateQueryParameter); + } + + public DatahubTariffFilter(Set chargeTypeCodes, Set notes) { + this(chargeTypeCodes, notes, DateQueryParameter.EMPTY); + } + + public DatahubTariffFilter(Set chargeTypeCodes, Set notes, + DateQueryParameter dateQueryParameter) { + this.chargeTypeCodes = chargeTypeCodes; + this.notes = notes; + this.dateQueryParameter = dateQueryParameter; + } + + public Collection getChargeTypeCodesAsStrings() { + return chargeTypeCodes.stream().map(c -> c.toString()).toList(); + } + + public Collection getNotes() { + return notes; + } + + public DateQueryParameter getDateQueryParameter() { + return dateQueryParameter; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java new file mode 100644 index 0000000000000..0d585ca282af3 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DatahubTariffFilterFactory.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Factory for creating a {@link DatahubTariffFilter} for a specific Grid Company GLN. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DatahubTariffFilterFactory { + + private static final String GLN_CERIUS = "5790000705184"; + private static final String GLN_DINEL = "5790000610099"; + private static final String GLN_ELEKTRUS = "5790000836239"; + private static final String GLN_ELINORD = "5790001095277"; + private static final String GLN_ELNET_MIDT = "5790001100520"; + private static final String GLN_ELNET_KONGERSLEV = "5790002502699"; + private static final String GLN_FLOW_ELNET = "5790000392551"; + private static final String GLN_HAMMEL_ELFORSYNING_NET = "5790001090166"; + private static final String GLN_HURUP_ELVAERK_NET = "5790000610839"; + private static final String GLN_IKAST_E1_NET = "5790000682102"; + private static final String GLN_KONSTANT = "5790000704842"; + private static final String GLN_L_NET = "5790001090111"; + private static final String GLN_N1 = "5790001089030"; + private static final String GLN_N1_RANDERS = "5790000681372"; + private static final String GLN_NETSELSKABET_ELVAERK = "5790000681075"; + private static final String GLN_NKE_ELNET = "5790001088231"; + private static final String GLN_NORD_ENERGI_NET = "5790000610877"; + private static final String GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET = "5790000395620"; + private static final String GLN_RADIUS = "5790000705689"; + private static final String GLN_RAH_NET = "5790000681327"; + private static final String GLN_RAVDEX = "5790000836727"; + private static final String GLN_TARM_ELVAERK_NET = "5790000706419"; + private static final String GLN_TREFOR_EL_NET = "5790000392261"; + private static final String GLN_TREFOR_EL_NET_OEST = "5790000706686"; + private static final String GLN_VEKSEL = "5790001088217"; + private static final String GLN_VORES_ELNET = "5790000610976"; + private static final String GLN_ZEANET = "5790001089375"; + + private static final String NOTE_NET_TARIFF = "Nettarif"; + private static final String NOTE_NET_TARIFF_C = NOTE_NET_TARIFF + " C"; + private static final String NOTE_NET_TARIFF_C_HOUR = NOTE_NET_TARIFF_C + " time"; + private static final String NOTE_SYSTEM_TARIFF = "Systemtarif"; + private static final String NOTE_ELECTRICITY_TAX = "Elafgift"; + private static final String NOTE_TRANSMISSION_NET_TARIFF = "Transmissions nettarif"; + + public static final LocalDate N1_CUTOFF_DATE = LocalDate.of(2023, 1, 1); + public static final LocalDate RADIUS_CUTOFF_DATE = LocalDate.of(2023, 1, 1); + public static final LocalDate KONSTANT_CUTOFF_DATE = LocalDate.of(2023, 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_N1: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD"), ChargeTypeCode.of("CD R")), Set.of(), + DateQueryParameter.of(N1_CUTOFF_DATE)); + case GLN_N1_RANDERS: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD")), Set.of(NOTE_NET_TARIFF_C), + DateQueryParameter.of(N1_CUTOFF_DATE)); + case GLN_NETSELSKABET_ELVAERK: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("0NCFF")), Set.of(NOTE_NET_TARIFF_C + " Flex"), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_NKE_ELNET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("94TR_C_ET")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_NORD_ENERGI_NET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TA031U200")), Set.of(NOTE_NET_TARIFF_C)); + case GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("Net C")), Set.of(NOTE_NET_TARIFF_C)); + case GLN_RADIUS: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("DT_C_01")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(RADIUS_CUTOFF_DATE)); + case GLN_RAH_NET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("RAH-C")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_RAVDEX: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-C")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_TARM_ELVAERK_NET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TEV-NT-01")), Set.of(NOTE_NET_TARIFF_C)); + case GLN_TREFOR_EL_NET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("C")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_TREFOR_EL_NET_OEST: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("46")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_VEKSEL: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-10")), + Set.of(NOTE_NET_TARIFF_C_HOUR + " NT-10")); + case GLN_VORES_ELNET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TNT1009")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + case GLN_ZEANET: + return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("43110")), Set.of(NOTE_NET_TARIFF_C_HOUR), + DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); + default: + return new DatahubTariffFilter(Set.of(), Set.of(NOTE_NET_TARIFF_C), + DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR)); + } + } + + public static DatahubTariffFilter getSystemTariff() { + return new DatahubTariffFilter(Set.of(), Set.of(NOTE_SYSTEM_TARIFF), + DateQueryParameter.of(ENERGINET_CUTOFF_DATE)); + } + + public static DatahubTariffFilter getElectricityTax() { + return new DatahubTariffFilter(Set.of(), Set.of(NOTE_ELECTRICITY_TAX), + DateQueryParameter.of(ENERGINET_CUTOFF_DATE)); + } + + public static DatahubTariffFilter getTransmissionNetTariff() { + return new DatahubTariffFilter(Set.of(), Set.of(NOTE_TRANSMISSION_NET_TARIFF), + DateQueryParameter.of(ENERGINET_CUTOFF_DATE)); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java new file mode 100644 index 0000000000000..70f996a97c56b --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameter.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import java.time.Duration; +import java.time.LocalDate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a query parameter of type {@link LocalDate} or a + * dynamic date defined as {@link DateQueryParameterType} with an optional offset. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DateQueryParameter { + + public static final DateQueryParameter EMPTY = new DateQueryParameter(); + + private @Nullable LocalDate date; + private @Nullable Duration offset; + private @Nullable DateQueryParameterType dateType; + + private DateQueryParameter() { + } + + public DateQueryParameter(LocalDate date) { + this.date = date; + } + + public DateQueryParameter(DateQueryParameterType dateType, Duration offset) { + this.dateType = dateType; + this.offset = offset; + } + + public DateQueryParameter(DateQueryParameterType dateType) { + this.dateType = dateType; + } + + @Override + public String toString() { + LocalDate date = this.date; + if (date != null) { + return date.toString(); + } + DateQueryParameterType dateType = this.dateType; + if (dateType != null) { + Duration offset = this.offset; + if (offset == null) { + return dateType.toString(); + } else { + return dateType.toString() + + (offset.isNegative() ? "-" + offset.abs().toString() : "+" + offset.toString()); + } + } + return "null"; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public static DateQueryParameter of(LocalDate localDate) { + return new DateQueryParameter(localDate); + } + + public static DateQueryParameter of(DateQueryParameterType dateType, Duration offset) { + if (offset.isZero()) { + return new DateQueryParameter(dateType); + } else { + return new DateQueryParameter(dateType, offset); + } + } + + public static DateQueryParameter of(DateQueryParameterType dateType) { + return new DateQueryParameter(dateType); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java new file mode 100644 index 0000000000000..3d951b27c8c15 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterType.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a dynamic date to be used in a query. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum DateQueryParameterType { + NOW("now"), + UTC_NOW("utcnow"), + START_OF_DAY("StartOfDay"), + START_OF_MONTH("StartOfMonth"), + START_OF_YEAR("StartOfYear"); + + private final String name; + + DateQueryParameterType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java new file mode 100644 index 0000000000000..4baa897b042bc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumber.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Global Location Number. + * See {@link https://www.gs1.org/standards/id-keys/gln}} + * The Global Location Number (GLN) can be used by companies to identify their locations. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class GlobalLocationNumber { + + public static final GlobalLocationNumber EMPTY = new GlobalLocationNumber(""); + + private static final int MAX_LENGTH = 13; + + private final String gln; + + public GlobalLocationNumber(String gln) { + if (gln.length() > MAX_LENGTH) { + throw new IllegalArgumentException("Maximum length exceeded: " + gln); + } + this.gln = gln; + } + + @Override + public String toString() { + return gln; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public boolean isValid() { + if (gln.length() != 13) { + return false; + } + + int checksum = 0; + for (int i = 13 - 2; i >= 0; i--) { + int digit = Character.getNumericValue(gln.charAt(i)); + checksum += (i % 2 == 0 ? digit : digit * 3); + } + int controlDigit = 10 - (checksum % 10); + if (controlDigit == 10) { + controlDigit = 0; + } + + return controlDigit == Character.getNumericValue(gln.charAt(13 - 1)); + } + + public static GlobalLocationNumber of(String gln) { + if (gln.isBlank()) { + return EMPTY; + } + return new GlobalLocationNumber(gln); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java new file mode 100644 index 0000000000000..4cbae11721db0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecord.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Record as part of {@link DatahubPricelistRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record DatahubPricelistRecord(@SerializedName("ValidFrom") LocalDateTime validFrom, + @SerializedName("ValidTo") LocalDateTime validTo, @SerializedName("ChargeTypeCode") String chargeTypeCode, + @SerializedName("Price1") BigDecimal price1, @SerializedName("Price2") BigDecimal price2, + @SerializedName("Price3") BigDecimal price3, @SerializedName("Price4") BigDecimal price4, + @SerializedName("Price5") BigDecimal price5, @SerializedName("Price6") BigDecimal price6, + @SerializedName("Price7") BigDecimal price7, @SerializedName("Price8") BigDecimal price8, + @SerializedName("Price9") BigDecimal price9, @SerializedName("Price10") BigDecimal price10, + @SerializedName("Price11") BigDecimal price11, @SerializedName("Price12") BigDecimal price12, + @SerializedName("Price13") BigDecimal price13, @SerializedName("Price14") BigDecimal price14, + @SerializedName("Price15") BigDecimal price15, @SerializedName("Price16") BigDecimal price16, + @SerializedName("Price17") BigDecimal price17, @SerializedName("Price18") BigDecimal price18, + @SerializedName("Price19") BigDecimal price19, @SerializedName("Price20") BigDecimal price20, + @SerializedName("Price21") BigDecimal price21, @SerializedName("Price22") BigDecimal price22, + @SerializedName("Price23") BigDecimal price23, @SerializedName("Price24") BigDecimal price24) { + + @Override + public LocalDateTime validTo() { + return Objects.isNull(validTo) ? LocalDateTime.MAX : validTo; + } + + @Override + public BigDecimal price2() { + return Objects.requireNonNullElse(price2, price1()); + } + + @Override + public BigDecimal price3() { + return Objects.requireNonNullElse(price3, price1()); + } + + @Override + public BigDecimal price4() { + return Objects.requireNonNullElse(price4, price1()); + } + + @Override + public BigDecimal price5() { + return Objects.requireNonNullElse(price5, price1()); + } + + @Override + public BigDecimal price6() { + return Objects.requireNonNullElse(price6, price1()); + } + + @Override + public BigDecimal price7() { + return Objects.requireNonNullElse(price7, price1()); + } + + @Override + public BigDecimal price8() { + return Objects.requireNonNullElse(price8, price1()); + } + + @Override + public BigDecimal price9() { + return Objects.requireNonNullElse(price9, price1()); + } + + @Override + public BigDecimal price10() { + return Objects.requireNonNullElse(price10, price1()); + } + + @Override + public BigDecimal price11() { + return Objects.requireNonNullElse(price11, price1()); + } + + @Override + public BigDecimal price12() { + return Objects.requireNonNullElse(price12, price1()); + } + + @Override + public BigDecimal price13() { + return Objects.requireNonNullElse(price13, price1()); + } + + @Override + public BigDecimal price14() { + return Objects.requireNonNullElse(price14, price1()); + } + + @Override + public BigDecimal price15() { + return Objects.requireNonNullElse(price15, price1()); + } + + @Override + public BigDecimal price16() { + return Objects.requireNonNullElse(price16, price1()); + } + + @Override + public BigDecimal price17() { + return Objects.requireNonNullElse(price17, price1()); + } + + @Override + public BigDecimal price18() { + return Objects.requireNonNullElse(price18, price1()); + } + + @Override + public BigDecimal price19() { + return Objects.requireNonNullElse(price19, price1()); + } + + @Override + public BigDecimal price20() { + return Objects.requireNonNullElse(price20, price1()); + } + + @Override + public BigDecimal price21() { + return Objects.requireNonNullElse(price21, price1()); + } + + @Override + public BigDecimal price22() { + return Objects.requireNonNullElse(price22, price1()); + } + + @Override + public BigDecimal price23() { + return Objects.requireNonNullElse(price23, price1()); + } + + @Override + public BigDecimal price24() { + return Objects.requireNonNullElse(price24, price1()); + } + + /** + * Get {@link Map} of tariffs with hour start as key. + * + * @return map with hourly tariffs + */ + public Map getTariffMap() { + Map tariffMap = new HashMap<>(); + + tariffMap.put(LocalTime.of(0, 0), price1()); + tariffMap.put(LocalTime.of(1, 0), price2()); + tariffMap.put(LocalTime.of(2, 0), price3()); + tariffMap.put(LocalTime.of(3, 0), price4()); + tariffMap.put(LocalTime.of(4, 0), price5()); + tariffMap.put(LocalTime.of(5, 0), price6()); + tariffMap.put(LocalTime.of(6, 0), price7()); + tariffMap.put(LocalTime.of(7, 0), price8()); + tariffMap.put(LocalTime.of(8, 0), price9()); + tariffMap.put(LocalTime.of(9, 0), price10()); + tariffMap.put(LocalTime.of(10, 0), price11()); + tariffMap.put(LocalTime.of(11, 0), price12()); + tariffMap.put(LocalTime.of(12, 0), price13()); + tariffMap.put(LocalTime.of(13, 0), price14()); + tariffMap.put(LocalTime.of(14, 0), price15()); + tariffMap.put(LocalTime.of(15, 0), price16()); + tariffMap.put(LocalTime.of(16, 0), price17()); + tariffMap.put(LocalTime.of(17, 0), price18()); + tariffMap.put(LocalTime.of(18, 0), price19()); + tariffMap.put(LocalTime.of(19, 0), price20()); + tariffMap.put(LocalTime.of(20, 0), price21()); + tariffMap.put(LocalTime.of(21, 0), price22()); + tariffMap.put(LocalTime.of(22, 0), price23()); + tariffMap.put(LocalTime.of(23, 0), price24()); + + return tariffMap; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java new file mode 100644 index 0000000000000..4aa6c28a53f65 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/DatahubPricelistRecords.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Received {@link DatahubPricelistRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record DatahubPricelistRecords(int total, String filters, int limit, String dataset, + DatahubPricelistRecord[] records) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java new file mode 100644 index 0000000000000..a657471c995cc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecord.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import java.math.BigDecimal; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Record as part of {@link ElspotpriceRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record ElspotpriceRecord(@SerializedName("HourUTC") Instant hour, + @SerializedName("SpotPriceDKK") BigDecimal spotPriceDKK, + @SerializedName("SpotPriceEUR") BigDecimal spotPriceEUR) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java new file mode 100644 index 0000000000000..87c8c2d71183f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/ElspotpriceRecords.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Received {@link ElspotpriceRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record ElspotpriceRecords(int total, String filters, String dataset, ElspotpriceRecord[] records) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java new file mode 100644 index 0000000000000..22c4ea66cbe3e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/InstantDeserializer.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.serialization; + +import java.lang.reflect.Type; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * The {@link InstantDeserializer} converts a formatted UTC string to {@link Instant}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class InstantDeserializer implements JsonDeserializer { + + @Override + public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) + throws JsonParseException { + String content = element.getAsString(); + // When writing this, the format of the provided UTC strings lacks the trailing 'Z'. + // In case this would be fixed in the future, gracefully support both with and without this. + return Instant.parse(content.endsWith("Z") ? content : content + "Z"); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java new file mode 100644 index 0000000000000..5b51db3d30b82 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateDeserializer.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.serialization; + +import java.lang.reflect.Type; +import java.time.LocalDate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * The {@link LocalDateDeserializer} converts a formatted string to {@link LocalDate}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class LocalDateDeserializer implements JsonDeserializer { + + @Override + public @Nullable LocalDate deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) + throws JsonParseException { + return LocalDate.parse(element.getAsString().substring(0, 10)); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java new file mode 100644 index 0000000000000..47851c05e0df3 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/serialization/LocalDateTimeDeserializer.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.serialization; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * The {@link LocalDateTimeDeserializer} converts a formatted string to {@link LocalDateTime}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class LocalDateTimeDeserializer implements JsonDeserializer { + + @Override + public @Nullable LocalDateTime deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) + throws JsonParseException { + return LocalDateTime.parse(element.getAsString()); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java new file mode 100644 index 0000000000000..d5dd072b0d0d0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/DatahubPriceConfiguration.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.config; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType; + +/** + * The {@link DatahubPriceConfiguration} class contains fields mapping channel configuration parameters. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DatahubPriceConfiguration extends PriceConfiguration { + + /** + * Comma-separated list of charge type codes, e.g. "CD,CD R". + */ + public String chargeTypeCodes = ""; + + /** + * Comma-separated list of notes, e.g. "Nettarif C". + */ + public String notes = ""; + + /** + * Query start date parameter expressed as either yyyy-mm-dd or one of StartOfDay, StartOfMonth or StartOfYear. + */ + public String start = ""; + + /** + * Check if any filter values are provided. + * + * @return true if either charge type codes, notes or query start date is provided. + */ + public boolean hasAnyFilterOverrides() { + return !chargeTypeCodes.isBlank() || !notes.isBlank() || !start.isBlank(); + } + + /** + * Get parsed set of charge type codes from comma-separated string. + * + * @return Set of charge type codes. + */ + public Set getChargeTypeCodes() { + return chargeTypeCodes.isBlank() ? new HashSet<>() + : new HashSet( + Arrays.stream(chargeTypeCodes.split(",")).map(ChargeTypeCode::new).toList()); + } + + /** + * Get parsed set of notes from comma-separated string. + * + * @return Set of notes. + */ + public Set getNotes() { + return notes.isBlank() ? new HashSet<>() : new HashSet(Arrays.asList(notes.split(","))); + } + + /** + * Get query start parameter. + * + * @return null if invalid, otherwise an initialized {@link DateQueryParameter}. + */ + public @Nullable DateQueryParameter getStart() { + if (start.isBlank()) { + return DateQueryParameter.EMPTY; + } + if (start.equals(DateQueryParameterType.START_OF_DAY.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_DAY); + } + if (start.equals(DateQueryParameterType.START_OF_MONTH.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_MONTH); + } + if (start.equals(DateQueryParameterType.START_OF_YEAR.toString())) { + return DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR); + } + try { + return DateQueryParameter.of(LocalDate.parse(start)); + } catch (DateTimeParseException e) { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java new file mode 100644 index 0000000000000..70cf0b45df7d9 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/EnergiDataServiceConfiguration.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.config; + +import java.util.Currency; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants; +import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; + +/** + * The {@link EnergiDataServiceConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class EnergiDataServiceConfiguration { + + /** + * Price area (DK1 = West of the Great Belt, DK2 = East of the Great Belt). + */ + public String priceArea = ""; + + /** + * Currency code for the prices. + */ + public String currencyCode = EnergiDataServiceBindingConstants.CURRENCY_DKK.getCurrencyCode(); + + /** + * Global Location Number of the Grid Company. + */ + public String gridCompanyGLN = ""; + + /** + * Global Location Number of Energinet. + */ + public String energinetGLN = "5790000432752"; + + /** + * Get {@link Currency} representing the configured currency code. + * + * @return Currency instance + */ + public Currency getCurrency() { + return Currency.getInstance(currencyCode); + } + + public GlobalLocationNumber getGridCompanyGLN() { + return GlobalLocationNumber.of(gridCompanyGLN); + } + + public GlobalLocationNumber getEnerginetGLN() { + return GlobalLocationNumber.of(energinetGLN); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java new file mode 100644 index 0000000000000..10628ba174bd2 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PriceConfiguration} class defines common configuration parameters for price + * channels. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class PriceConfiguration { + + public static final String INCLUDE_VAT = "includeVAT"; + + /** + * Add VAT to amount based on regional settings. + */ + public boolean includeVAT = false; +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java new file mode 100644 index 0000000000000..ab2c522e49a84 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link DataServiceException} is a generic Energi Data Service exception thrown in case + * of communication failure or unexpected response. It is intended to be derived by + * specialized exceptions. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DataServiceException extends Exception { + + private static final long serialVersionUID = 1L; + private int httpStatus = 0; + + public DataServiceException(String message) { + super(message); + } + + public DataServiceException(Throwable cause) { + super(cause); + } + + public DataServiceException(String message, Throwable cause) { + super(message, cause); + } + + public DataServiceException(String message, int httpStatus) { + super(message); + this.httpStatus = httpStatus; + } + + public int getHttpStatus() { + return httpStatus; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java new file mode 100644 index 0000000000000..95814c24a0dd5 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/MissingPriceException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link MissingPriceException} is thrown when there are no prices + * available in the requested interval, e.g. when performing a calculation. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class MissingPriceException extends Exception { + + private static final long serialVersionUID = 1L; + + public MissingPriceException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java new file mode 100644 index 0000000000000..4212d80857d17 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.factory; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EnergiDataServiceHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.energidataservice", service = ThingHandlerFactory.class) +public class EnergiDataServiceHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE); + + private final HttpClient httpClient; + private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; + + @Activate + public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference LocaleProvider localeProvider, final @Reference TimeZoneProvider timeZoneProvider, + ComponentContext componentContext) { + super.activate(componentContext); + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_SERVICE.equals(thingTypeUID)) { + return new EnergiDataServiceHandler(thing, httpClient, localeProvider, timeZoneProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java new file mode 100644 index 0000000000000..6d8bf4e1dbead --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java @@ -0,0 +1,561 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.handler; + +import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Currency; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.energidataservice.internal.ApiController; +import org.openhab.binding.energidataservice.internal.CacheManager; +import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions; +import org.openhab.binding.energidataservice.internal.api.ChargeType; +import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; +import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; +import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; +import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType; +import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; +import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration; +import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration; +import org.openhab.binding.energidataservice.internal.config.PriceConfiguration; +import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link EnergiDataServiceHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class EnergiDataServiceHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class); + private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; + private final ApiController apiController; + private final CacheManager cacheManager; + private final Gson gson = new Gson(); + + private EnergiDataServiceConfiguration config; + private RetryStrategy retryPolicy = RetryPolicyFactory.initial(); + private @Nullable ScheduledFuture refreshFuture; + private @Nullable ScheduledFuture priceUpdateFuture; + + private record Price(String hourStart, BigDecimal spotPrice, String spotPriceCurrency, + @Nullable BigDecimal netTariff, @Nullable BigDecimal systemTariff, @Nullable BigDecimal electricityTax, + @Nullable BigDecimal transmissionNetTariff) { + } + + public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, LocaleProvider localeProvider, + TimeZoneProvider timeZoneProvider) { + super(thing); + this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; + this.apiController = new ApiController(httpClient, timeZoneProvider); + this.cacheManager = new CacheManager(); + + // Default configuration + this.config = new EnergiDataServiceConfiguration(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (!(command instanceof RefreshType)) { + return; + } + + if (ELECTRICITY_CHANNELS.contains(channelUID.getId())) { + refreshElectricityPrices(); + } + } + + @Override + public void initialize() { + config = getConfigAs(EnergiDataServiceConfiguration.class); + + if (config.priceArea.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.conf-error.no-price-area"); + return; + } + GlobalLocationNumber gln = config.getGridCompanyGLN(); + if (!gln.isEmpty() && !gln.isValid()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.conf-error.invalid-grid-company-gln"); + return; + } + gln = config.getEnerginetGLN(); + if (!gln.isEmpty() && !gln.isValid()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.conf-error.invalid-energinet-gln"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + refreshFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS); + } + + @Override + public void dispose() { + ScheduledFuture refreshFuture = this.refreshFuture; + if (refreshFuture != null) { + refreshFuture.cancel(true); + this.refreshFuture = null; + } + ScheduledFuture priceUpdateFuture = this.priceUpdateFuture; + if (priceUpdateFuture != null) { + priceUpdateFuture.cancel(true); + this.priceUpdateFuture = null; + } + + cacheManager.clear(); + } + + @Override + public Collection> getServices() { + return Set.of(EnergiDataServiceActions.class); + } + + private void refreshElectricityPrices() { + RetryStrategy retryPolicy; + try { + if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { + downloadSpotPrices(); + } + + if (isLinked(CHANNEL_CURRENT_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + downloadNetTariffs(); + } + + if (isLinked(CHANNEL_CURRENT_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + downloadSystemTariffs(); + } + + if (isLinked(CHANNEL_CURRENT_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) { + downloadElectricityTaxes(); + } + + if (isLinked(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + downloadTransmissionNetTariffs(); + } + + updateStatus(ThingStatus.ONLINE); + updatePrices(); + + if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (cacheManager.getNumberOfFutureSpotPrices() < 13) { + retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing(DAILY_REFRESH_TIME_CET, + NORD_POOL_TIMEZONE); + } else { + retryPolicy = RetryPolicyFactory.atFixedTime(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE); + } + } else { + retryPolicy = RetryPolicyFactory.atFixedTime(LocalTime.MIDNIGHT, timeZoneProvider.getTimeZone()); + } + } catch (DataServiceException e) { + if (e.getHttpStatus() != 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + HttpStatus.getCode(e.getHttpStatus()).getMessage()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + } + if (e.getCause() != null) { + logger.debug("Error retrieving prices", e); + } + retryPolicy = RetryPolicyFactory.fromThrowable(e); + } catch (InterruptedException e) { + logger.debug("Refresh job interrupted"); + Thread.currentThread().interrupt(); + return; + } + + rescheduleRefreshJob(retryPolicy); + } + + private void downloadSpotPrices() throws InterruptedException, DataServiceException { + if (cacheManager.areSpotPricesFullyCached()) { + logger.debug("Cached spot prices still valid, skipping download."); + return; + } + DateQueryParameter start; + if (cacheManager.areHistoricSpotPricesCached()) { + start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW); + } else { + start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, + Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS)); + } + Map properties = editProperties(); + ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(), + start, properties); + cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency()); + updateProperties(properties); + } + + private void downloadNetTariffs() throws InterruptedException, DataServiceException { + if (config.getGridCompanyGLN().isEmpty()) { + return; + } + if (cacheManager.areNetTariffsValidTomorrow()) { + logger.debug("Cached net tariffs still valid, skipping download."); + cacheManager.updateNetTariffs(); + } else { + cacheManager.putNetTariffs(downloadPriceLists(config.getGridCompanyGLN(), getNetTariffFilter())); + } + } + + private void downloadSystemTariffs() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areSystemTariffsValidTomorrow()) { + logger.debug("Cached system tariffs still valid, skipping download."); + cacheManager.updateSystemTariffs(); + } else { + cacheManager.putSystemTariffs( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getSystemTariff())); + } + } + + private void downloadElectricityTaxes() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areElectricityTaxesValidTomorrow()) { + logger.debug("Cached electricity taxes still valid, skipping download."); + cacheManager.updateElectricityTaxes(); + } else { + cacheManager.putElectricityTaxes( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getElectricityTax())); + } + } + + private void downloadTransmissionNetTariffs() throws InterruptedException, DataServiceException { + GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN(); + if (globalLocationNumber.isEmpty()) { + return; + } + if (cacheManager.areTransmissionNetTariffsValidTomorrow()) { + logger.debug("Cached transmission net tariffs still valid, skipping download."); + cacheManager.updateTransmissionNetTariffs(); + } else { + cacheManager.putTransmissionNetTariffs( + downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getTransmissionNetTariff())); + } + } + + private Collection downloadPriceLists(GlobalLocationNumber globalLocationNumber, + DatahubTariffFilter filter) throws InterruptedException, DataServiceException { + Map properties = editProperties(); + Collection records = apiController.getDatahubPriceLists(globalLocationNumber, + ChargeType.Tariff, filter, properties); + updateProperties(properties); + + return records; + } + + private DatahubTariffFilter getNetTariffFilter() { + Channel channel = getThing().getChannel(CHANNEL_CURRENT_NET_TARIFF); + if (channel == null) { + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration() + .as(DatahubPriceConfiguration.class); + + if (!datahubPriceConfiguration.hasAnyFilterOverrides()) { + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + DateQueryParameter start = datahubPriceConfiguration.getStart(); + if (start == null) { + logger.warn("Invalid channel configuration parameter 'start': {}", datahubPriceConfiguration.start); + return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); + } + + Set chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes(); + Set notes = datahubPriceConfiguration.getNotes(); + if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) { + // Completely override filter. + return new DatahubTariffFilter(chargeTypeCodes, notes, start); + } else { + // Only override start date in pre-configured filter. + return new DatahubTariffFilter(DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN), start); + } + } + + private void updatePrices() { + cacheManager.cleanup(); + + updateCurrentSpotPrice(); + updateCurrentTariff(CHANNEL_CURRENT_NET_TARIFF, cacheManager.getNetTariff()); + updateCurrentTariff(CHANNEL_CURRENT_SYSTEM_TARIFF, cacheManager.getSystemTariff()); + updateCurrentTariff(CHANNEL_CURRENT_ELECTRICITY_TAX, cacheManager.getElectricityTax()); + updateCurrentTariff(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, cacheManager.getTransmissionNetTariff()); + updateHourlyPrices(); + + reschedulePriceUpdateJob(); + } + + private void updateCurrentSpotPrice() { + if (!isLinked(CHANNEL_CURRENT_SPOT_PRICE)) { + return; + } + BigDecimal price = getVATAdjustedPrice(cacheManager.getSpotPrice(), CHANNEL_CURRENT_SPOT_PRICE); + updateState(CHANNEL_CURRENT_SPOT_PRICE, price != null ? new DecimalType(price) : UnDefType.UNDEF); + } + + private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) { + if (!isLinked(channelId)) { + return; + } + BigDecimal price = getVATAdjustedPrice(tariff, channelId); + updateState(channelId, price != null ? new DecimalType(price) : UnDefType.UNDEF); + } + + private @Nullable BigDecimal getVATAdjustedPrice(@Nullable BigDecimal price, String channelId) { + if (price == null) { + return price; + } + Channel channel = getThing().getChannel(channelId); + if (channel == null) { + return price; + } + Object obj = channel.getConfiguration().get(PriceConfiguration.INCLUDE_VAT); + if (obj == null) { + return price; + } + Boolean includeVAT = (Boolean) obj; + if (includeVAT) { + return price.multiply(getVATPercentageFactor()); + } + return price; + } + + private BigDecimal getVATPercentageFactor() { + String country = localeProvider.getLocale().getCountry(); + switch (country) { + case "DK": + case "NO": + case "SE": + return new BigDecimal("1.25"); + case "DE": + return new BigDecimal("1.19"); + default: + logger.debug("No VAT rate for country {}", country); + return BigDecimal.ONE; + } + } + + private void updateHourlyPrices() { + if (!isLinked(CHANNEL_HOURLY_PRICES)) { + return; + } + Map spotPriceMap = cacheManager.getSpotPrices(); + Price[] targetPrices = new Price[spotPriceMap.size()]; + List> sourcePrices = spotPriceMap.entrySet().stream() + .sorted(Map.Entry. comparingByKey()).toList(); + + int i = 0; + for (Entry sourcePrice : sourcePrices) { + Instant hourStart = sourcePrice.getKey(); + BigDecimal netTariff = cacheManager.getNetTariff(hourStart); + BigDecimal systemTariff = cacheManager.getSystemTariff(hourStart); + BigDecimal electricityTax = cacheManager.getElectricityTax(hourStart); + BigDecimal transmissionNetTariff = cacheManager.getTransmissionNetTariff(hourStart); + targetPrices[i++] = new Price(hourStart.toString(), sourcePrice.getValue(), config.currencyCode, netTariff, + systemTariff, electricityTax, transmissionNetTariff); + } + updateState(CHANNEL_HOURLY_PRICES, new StringType(gson.toJson(targetPrices))); + } + + /** + * Get the configured {@link Currency} for spot prices. + * + * @return Spot price currency + */ + public Currency getCurrency() { + return config.getCurrency(); + } + + /** + * Get cached spot prices or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future spot prices + */ + public Map getSpotPrices() { + try { + downloadSpotPrices(); + } catch (DataServiceException e) { + logger.warn("Error retrieving spot prices"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return cacheManager.getSpotPrices(); + } + + /** + * Get cached net tariffs or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future net tariffs + */ + public Map getNetTariffs() { + try { + downloadNetTariffs(); + } catch (DataServiceException e) { + logger.warn("Error retrieving net tariffs"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return cacheManager.getNetTariffs(); + } + + /** + * Get cached system tariffs or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future system tariffs + */ + public Map getSystemTariffs() { + try { + downloadSystemTariffs(); + } catch (DataServiceException e) { + logger.warn("Error retrieving system tariffs"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return cacheManager.getSystemTariffs(); + } + + /** + * Get cached electricity taxes or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future electricity taxes + */ + public Map getElectricityTaxes() { + try { + downloadElectricityTaxes(); + } catch (DataServiceException e) { + logger.warn("Error retrieving electricity taxes"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return cacheManager.getElectricityTaxes(); + } + + /** + * Return cached transmission net tariffs or try once to download them if not cached + * (usually if no items are linked). + * + * @return Map of future transmissions net tariffs + */ + public Map getTransmissionNetTariffs() { + try { + downloadTransmissionNetTariffs(); + } catch (DataServiceException e) { + logger.warn("Error retrieving transmission net tariffs"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return cacheManager.getTransmissionNetTariffs(); + } + + private void reschedulePriceUpdateJob() { + ScheduledFuture priceUpdateJob = this.priceUpdateFuture; + if (priceUpdateJob != null) { + // Do not interrupt ourselves. + priceUpdateJob.cancel(false); + this.priceUpdateFuture = null; + } + + Instant now = Instant.now(); + long millisUntilNextClockHour = Duration + .between(now, now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS)).toMillis() + 1; + this.priceUpdateFuture = scheduler.schedule(this::updatePrices, millisUntilNextClockHour, + TimeUnit.MILLISECONDS); + logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour); + } + + private void rescheduleRefreshJob(RetryStrategy retryPolicy) { + // Preserve state of previous retry policy when configuration is the same. + if (!retryPolicy.equals(this.retryPolicy)) { + this.retryPolicy = retryPolicy; + } + + ScheduledFuture refreshJob = this.refreshFuture; + + long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds(); + Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh); + this.refreshFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh, + TimeUnit.SECONDS); + logger.debug("Refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT); + updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone()) + .truncatedTo(ChronoUnit.SECONDS).format(formatter)); + + if (refreshJob != null) { + refreshJob.cancel(true); + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java new file mode 100644 index 0000000000000..00f0ab89cc4e8 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalTime; +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.binding.energidataservice.internal.retry.strategy.ExponentialBackoff; +import org.openhab.binding.energidataservice.internal.retry.strategy.FixedTime; +import org.openhab.binding.energidataservice.internal.retry.strategy.Linear; + +/** + * This factory defines policies for determining appropriate {@link RetryStrategy} based + * on scenario. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class RetryPolicyFactory { + + /** + * Determine {@link RetryStrategy} from {@link Throwable}. + * + * @param e thrown exception + * @return retry strategy + */ + public static RetryStrategy fromThrowable(Throwable e) { + if (e instanceof DataServiceException dse) { + switch (dse.getHttpStatus()) { + case HttpStatus.TOO_MANY_REQUESTS_429: + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(30)); + default: + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); + } + } + + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); + } + + /** + * Default {@link RetryStrategy} with one retry per day. + * This is intended as a dummy strategy until replaced by a concrete one. + * + * @return retry strategy + */ + public static RetryStrategy initial() { + return new Linear().withMinimum(Duration.ofDays(1)); + } + + /** + * Determine {@link RetryStrategy} for next expected data publishing. + * + * @param localTime the time of daily data request in local time-zone + * @param zoneId the local time-zone + * @return retry strategy + */ + public static RetryStrategy atFixedTime(LocalTime localTime, ZoneId zoneId) { + return new FixedTime(localTime, Clock.system(zoneId)).withJitter(1); + } + + /** + * Determine {@link RetryStrategy} when expected spot price data is missing. + * + * @param utcTime the time of daily data request in UTC time-zone + * @return retry strategy + */ + public static RetryStrategy whenExpectedSpotPriceDataMissing(LocalTime localTime, ZoneId zoneId) { + LocalTime now = LocalTime.now(zoneId); + if (now.isAfter(localTime)) { + return new ExponentialBackoff().withMinimum(Duration.ofMinutes(10)).withJitter(0.2); + } + return atFixedTime(localTime, zoneId); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java new file mode 100644 index 0000000000000..eb67ba8300056 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryStrategy.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This interface defines a retry strategy for failed network + * requests. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public interface RetryStrategy { + /** + * Get {@link Duration} until next attempt. This will auto-increment number of + * attempts, so should only be called once after each failed request. + * + * @return duration until next attempt according to strategy + */ + Duration getDuration(); +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java new file mode 100644 index 0000000000000..4b510ae22cacc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoff.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for exponential backoff with jitter. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ExponentialBackoff implements RetryStrategy { + + private int attempts = 0; + private int factor = 2; + private double jitter = 0.0; + private Duration minimum = Duration.ofMillis(100); + private Duration maximum = Duration.ofHours(6); + + public ExponentialBackoff() { + } + + @Override + public Duration getDuration() { + long minimum = this.minimum.toMillis(); + long maximum = this.maximum.toMillis(); + long duration = minimum * (long) Math.pow(this.factor, this.attempts++); + if (jitter != 0.0) { + double rand = Math.random(); + if ((((int) Math.floor(rand * 10)) & 1) == 0) { + duration += (long) (rand * jitter * duration); + } else { + duration -= (long) (rand * jitter * duration); + } + } + if (duration < minimum) { + duration = minimum; + } + if (duration > maximum) { + duration = maximum; + } + return Duration.ofMillis(duration); + } + + public ExponentialBackoff withFactor(int factor) { + this.factor = factor; + return this; + } + + public ExponentialBackoff withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + public ExponentialBackoff withMinimum(Duration minimum) { + this.minimum = minimum; + return this; + } + + public ExponentialBackoff withMaximum(Duration maximum) { + this.maximum = maximum; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ExponentialBackoff)) { + return false; + } + ExponentialBackoff other = (ExponentialBackoff) o; + + return this.factor == other.factor && this.jitter == other.jitter && this.minimum.equals(other.minimum) + && this.maximum.equals(other.maximum); + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + factor; + result = prime * result + (int) jitter * 100; + result = prime * result + (int) minimum.toMillis(); + result = prime * result + (int) maximum.toMillis(); + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java new file mode 100644 index 0000000000000..98d78a0485a5f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTime.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for a fixed time. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class FixedTime implements RetryStrategy { + + private final Clock clock; + + private LocalTime localTime; + private double jitter = 0.0; + + public FixedTime(LocalTime localTime, Clock clock) { + this.localTime = localTime; + this.clock = clock; + } + + @Override + public Duration getDuration() { + LocalTime now = LocalTime.now(clock); + LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(clock), localTime); + if (now.isAfter(localTime)) { + localDateTime = localDateTime.plusDays(1); + } + + Duration base = Duration.between(LocalDateTime.now(clock), localDateTime); + if (jitter == 0.0) { + return base; + } + + long duration = base.toMillis(); + double rand = Math.random(); + duration += (long) (rand * jitter * 1000 * 60); + + return Duration.ofMillis(duration); + } + + public FixedTime withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FixedTime)) { + return false; + } + FixedTime other = (FixedTime) o; + + return this.jitter == other.jitter && this.localTime.equals(other.localTime); + } + + @Override + public final int hashCode() { + final int result = 1; + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java new file mode 100644 index 0000000000000..99bd57a460b7d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/strategy/Linear.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * This implements a {@link RetryStrategy} for linear retry with jitter. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class Linear implements RetryStrategy { + + private double jitter = 0.0; + private Duration minimum = Duration.ofMillis(100); + private Duration maximum = Duration.ofHours(6); + + public Linear() { + } + + @Override + public Duration getDuration() { + long minimum = this.minimum.toMillis(); + long maximum = this.maximum.toMillis(); + long duration = minimum; + if (jitter != 0.0) { + double rand = Math.random(); + if ((((int) Math.floor(rand * 10)) & 1) == 0) { + duration += (long) (rand * jitter * duration); + } else { + duration -= (long) (rand * jitter * duration); + } + } + if (duration < minimum) { + duration = minimum; + } + if (duration > maximum) { + duration = maximum; + } + return Duration.ofMillis(duration); + } + + public Linear withJitter(double jitter) { + this.jitter = jitter; + return this; + } + + public Linear withMinimum(Duration minimum) { + this.minimum = minimum; + return this; + } + + public Linear withMaximum(Duration maximum) { + this.maximum = maximum; + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Linear)) { + return false; + } + Linear other = (Linear) o; + + return this.jitter == other.jitter && this.minimum.equals(other.minimum) && this.maximum.equals(other.maximum); + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) jitter * 100; + result = prime * result + (int) minimum.toMillis(); + result = prime * result + (int) maximum.toMillis(); + + return result; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..c4a4a405e5586 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Energi Data Service Binding + This is the binding for Energi Data Service. + cloud + DK,NO,SE + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..be4a186f2c28d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,104 @@ + + + + + + + Price area for spot prices (same as bidding zone). + false + + + + + + + + Currency code in which to obtain spot prices. + DKK + + + + + + + + Global Location Number of the grid company. + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Global Location Number of Energinet. + true + 5790000432752 + + + + + + + Add VAT to amount based on regional settings. + + + + + + + Add VAT to amount based on regional settings. + + + + Comma-separated list of charge type codes. + true + + + + Comma-separated list of notes. + true + + + + Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, + StartOfMonth or StartOfYear. + false + + + + + + true + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties new file mode 100644 index 0000000000000..441f2b974257d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties @@ -0,0 +1,108 @@ +# add-on + +addon.energidataservice.name = Energi Data Service Binding +addon.energidataservice.description = This is the binding for Energi Data Service. + +# thing types + +thing-type.energidataservice.service.label = Energi Data Service +thing-type.energidataservice.service.description = This thing represents the Energi Data Service API. + +# thing types config + +thing-type.config.energidataservice.service.currencyCode.label = Currency Code +thing-type.config.energidataservice.service.currencyCode.description = Currency code in which to obtain spot prices. +thing-type.config.energidataservice.service.currencyCode.option.DKK = Danish Krone +thing-type.config.energidataservice.service.currencyCode.option.EUR = Euro +thing-type.config.energidataservice.service.energinetGLN.label = Energinet GLN +thing-type.config.energidataservice.service.energinetGLN.description = Global Location Number of Energinet. +thing-type.config.energidataservice.service.gridCompanyGLN.label = Grid Company GLN +thing-type.config.energidataservice.service.gridCompanyGLN.description = Global Location Number of the grid company. +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705184 = Cerius +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610099 = Dinel +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790002502699 = El-net Kongerslev +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836239 = Elektrus +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001095277 = Elinord +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001100520 = Elnet Midt +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392551 = FLOW Elnet +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090166 = Hammel Elforsyning Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610839 = Hurup Elværk Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000682102 = Ikast El Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000704842 = Konstant +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090111 = L-Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089030 = N1 +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681372 = N1 Randers +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681075 = Netselskabet Elværk +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088231 = NKE-Elnet +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610877 = Nord Energi Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000395620 = Nordvestjysk Elforsyning (NOE Net) +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705689 = Radius +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681327 = RAH +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836727 = Ravdex +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706419 = Tarm Elværk Net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392261 = TREFOR El-net +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706686 = TREFOR El-net Øst +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088217 = Veksel +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610976 = Vores Elnet +thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089375 = Zeanet +thing-type.config.energidataservice.service.priceArea.label = Price Area +thing-type.config.energidataservice.service.priceArea.description = Price area for spot prices (same as bidding zone). +thing-type.config.energidataservice.service.priceArea.option.DK1 = West of the Great Belt +thing-type.config.energidataservice.service.priceArea.option.DK2 = East of the Great Belt + +# channel group types + +channel-group-type.energidataservice.electricity.label = Electricity +channel-group-type.energidataservice.electricity.description = Channels related to electricity +channel-group-type.energidataservice.electricity.channel.currentElectricityTax.label = Current Electricity Tax +channel-group-type.energidataservice.electricity.channel.currentElectricityTax.description = Electricity Tax in DKK per kWh for current hour. +channel-group-type.energidataservice.electricity.channel.currentNetTariff.label = Current Net Tariff +channel-group-type.energidataservice.electricity.channel.currentNetTariff.description = Net tariff in DKK per kWh for current hour. +channel-group-type.energidataservice.electricity.channel.currentSpotPrice.label = Current Spot Price +channel-group-type.energidataservice.electricity.channel.currentSpotPrice.description = Spot price in DKK or EUR per kWh for current hour. +channel-group-type.energidataservice.electricity.channel.currentSystemTariff.label = Current System Tariff +channel-group-type.energidataservice.electricity.channel.currentSystemTariff.description = System tariff in DKK per kWh for current hour. +channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.label = Current Transmission Tariff +channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.description = Transmission Net Tariff in DKK per kWh for current hour. + +# channel types + +channel-type.energidataservice.datahubPrice.label = Datahub Price +channel-type.energidataservice.datahubPrice.description = Datahub price. +channel-type.energidataservice.hourlyPrices.label = Hourly Prices +channel-type.energidataservice.hourlyPrices.description = JSON array with hourly prices from 12 hours ago and onward. +channel-type.energidataservice.spotPrice.label = Spot Price +channel-type.energidataservice.spotPrice.description = Spot price. + +# channel types config + +channel-type.config.energidataservice.datahub-price.chargeTypeCodes.label = Charge Type Code Filters +channel-type.config.energidataservice.datahub-price.chargeTypeCodes.description = Comma-separated list of charge type codes. +channel-type.config.energidataservice.datahub-price.includeVAT.label = Include VAT +channel-type.config.energidataservice.datahub-price.includeVAT.description = Add VAT to amount based on regional settings. +channel-type.config.energidataservice.datahub-price.notes.label = Note Filters +channel-type.config.energidataservice.datahub-price.notes.description = Comma-separated list of notes. +channel-type.config.energidataservice.datahub-price.start.label = Query Start Date +channel-type.config.energidataservice.datahub-price.start.description = Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear. +channel-type.config.energidataservice.datahub-price.start.option.StartOfDay = Start of day +channel-type.config.energidataservice.datahub-price.start.option.StartOfMonth = Start of month +channel-type.config.energidataservice.datahub-price.start.option.StartOfYear = Start of year +channel-type.config.energidataservice.spot-price.includeVAT.label = Include VAT +channel-type.config.energidataservice.spot-price.includeVAT.description = Add VAT to amount based on regional settings. + +# thing status descriptions + +offline.conf-error.no-price-area = Price area must be set +offline.conf-error.invalid-grid-company-gln = Invalid grid company GLN +offline.conf-error.invalid-energinet-gln = Invalid Energinet GLN + +# actions + +action.calculate-cheapest-period.label = calculate cheapest period +action.calculate-cheapest-period.description = calculate cheapest period for using power according to a supplied timetable (excl. VAT) +action.calculate-price.label = calculate price +action.calculate-price.description = calculate price for power consumption in period excl. VAT +action.get-prices.label = get prices +action.get-prices.description = get hourly prices excl. VAT +action.get-prices.priceElements.label = price elements +action.get-prices.priceElements.description = comma-separated list of price elements to include in sums diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml new file mode 100644 index 0000000000000..e365653e0022e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml @@ -0,0 +1,35 @@ + + + + + + Channels related to electricity + + + + Spot price in DKK or EUR per kWh for current hour. + + + + Net tariff in DKK per kWh for current hour. + + + + System tariff in DKK per kWh for current hour. + + + + Electricity Tax in DKK per kWh for current hour. + + + + Transmission Net Tariff in DKK per kWh for current hour. + + + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 0000000000000..ffab302bd670e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,33 @@ + + + + + Number + + Spot price. + Price + + + + + + Number + + Datahub price. + Price + + + + + + String + + JSON array with hourly prices from 12 hours ago and onward. + Price + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml new file mode 100644 index 0000000000000..84e459e170b3e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml @@ -0,0 +1,19 @@ + + + + + + + This thing represents the Energi Data Service API. + + + + + + + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java new file mode 100644 index 0000000000000..5deecd5abd94c --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/CacheManagerTest.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; + +/** + * Tests for {@link CacheManager}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class CacheManagerTest { + + @Test + void areSpotPricesFullyCachedToday() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T20:00:00Z"); + Instant last = Instant.parse("2023-02-07T22:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areSpotPricesFullyCached(), is(true)); + } + + @Test + void areSpotPricesFullyCachedTodayMissingAtStart() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T21:00:00Z"); + Instant last = Instant.parse("2023-02-07T22:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areSpotPricesFullyCached(), is(false)); + } + + @Test + void areSpotPricesFullyCachedTodayMissingAtEnd() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T20:00:00Z"); + Instant last = Instant.parse("2023-02-07T21:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areSpotPricesFullyCached(), is(false)); + } + + @Test + void areSpotPricesFullyCachedTodayOtherTimezoneIsIgnored() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T20:00:00Z"); + Instant last = Instant.parse("2023-02-07T22:00:00Z"); + Clock clock = Clock.fixed(now, ZoneId.of("Asia/Tokyo")); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areSpotPricesFullyCached(), is(true)); + } + + @Test + void areSpotPricesFullyCachedTomorrow() { + Instant now = Instant.parse("2023-02-07T12:00:00Z"); + Instant first = Instant.parse("2023-02-07T00:00:00Z"); + Instant last = Instant.parse("2023-02-08T22:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areSpotPricesFullyCached(), is(true)); + } + + @Test + void areHistoricSpotPricesCached() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T20:00:00Z"); + Instant last = Instant.parse("2023-02-07T07:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areHistoricSpotPricesCached(), is(true)); + } + + @Test + void areHistoricSpotPricesCachedFirstHourMissing() { + Instant now = Instant.parse("2023-02-07T08:38:47Z"); + Instant first = Instant.parse("2023-02-06T21:00:00Z"); + Instant last = Instant.parse("2023-02-07T08:00:00Z"); + Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE); + CacheManager cacheManager = new CacheManager(clock); + populateWithSpotPrices(cacheManager, first, last); + assertThat(cacheManager.areHistoricSpotPricesCached(), is(false)); + } + + private void populateWithSpotPrices(CacheManager cacheManager, Instant first, Instant last) { + int size = (int) Duration.between(first, last).getSeconds() / 60 / 60 + 1; + ElspotpriceRecord[] records = new ElspotpriceRecord[size]; + int i = 0; + for (Instant hourStart = first; !hourStart.isAfter(last); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) { + records[i++] = new ElspotpriceRecord(hourStart, BigDecimal.ONE, BigDecimal.ZERO); + } + cacheManager.putSpotPrices(records, EnergiDataServiceBindingConstants.CURRENCY_DKK); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java new file mode 100644 index 0000000000000..689966a500bd4 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/PriceListParserTest.java @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; +import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Tests for {@link PriceListParser}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class PriceListParserTest { + + private Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) + .create(); + + private T getObjectFromJson(String filename, Class clazz) throws IOException { + try (InputStream inputStream = PriceListParserTest.class.getResourceAsStream(filename)) { + if (inputStream == null) { + throw new IOException("Input stream is null"); + } + byte[] bytes = inputStream.readAllBytes(); + if (bytes == null) { + throw new IOException("Resulting byte-array empty"); + } + String json = new String(bytes, StandardCharsets.UTF_8); + return Objects.requireNonNull(gson.fromJson(json, clazz)); + } + } + + @Test + void toHourlyNoChanges() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-01-23T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(48)); + assertThat(tariffMap.get(Instant.parse("2023-01-23T15:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-01-23T16:00:00Z")), is(equalTo(new BigDecimal("1.05619")))); + assertThat(tariffMap.get(Instant.parse("2023-01-24T15:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-01-24T16:00:00Z")), is(equalTo(new BigDecimal("1.05619")))); + } + + @Test + void toHourlyNewTariffTomorrowWhenSummertime() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-03-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(48)); + assertThat(tariffMap.get(Instant.parse("2023-03-31T14:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-03-31T15:00:00Z")), is(equalTo(new BigDecimal("1.05619")))); + assertThat(tariffMap.get(Instant.parse("2023-04-01T14:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-04-01T15:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + } + + @Test + void toHourlyNewTariffAtMidnight() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD"); + + assertThat(tariffMap.size(), is(48)); + assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717")))); + assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + } + + @Test + void toHourlyDiscount() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), + "CD R"); + + assertThat(tariffMap.size(), is(48)); + assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717")))); + assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.0")))); + assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.0")))); + } + + @Test + void toHourlyTariffAndDiscountIsSum() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2022-11-30T15:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(45)); + assertThat(tariffMap.get(Instant.parse("2022-11-30T15:00:00Z")), is(equalTo(new BigDecimal("0.387517")))); + assertThat(tariffMap.get(Instant.parse("2022-11-30T16:00:00Z")), is(equalTo(new BigDecimal("0.973404")))); + } + + @Test + void toHourlyTariffAndDiscountIsFree() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(48)); + assertThat(tariffMap.get(Instant.parse("2022-12-31T16:00:00Z")), is(equalTo(new BigDecimal("0.000000")))); + assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.000000")))); + assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + } + + @Test + void toHourlyFixedTariff() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2022-12-31T23:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistNordEnergi.json", + DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(25)); // No records in dataset before 2023-01-01 + for (Instant i = Instant.parse("2022-12-31T23:00:00Z"); i + .isBefore(Instant.parse("2023-01-02T00:00:00Z")); i = i.plus(1, ChronoUnit.HOURS)) { + assertThat(tariffMap.get(i), is(equalTo(new BigDecimal("0.245")))); + } + } + + @Test + void toHourlyDailyTariffs() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-01-28T04:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistTrefor.json", + DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(56)); + assertThat(tariffMap.get(Instant.parse("2023-01-28T04:00:00Z")), is(equalTo(new BigDecimal("0.2581")))); + assertThat(tariffMap.get(Instant.parse("2023-01-28T05:00:00Z")), is(equalTo(new BigDecimal("0.7742")))); + assertThat(tariffMap.get(Instant.parse("2023-01-28T16:00:00Z")), is(equalTo(new BigDecimal("2.3227")))); + assertThat(tariffMap.get(Instant.parse("2023-01-28T20:00:00Z")), is(equalTo(new BigDecimal("0.7742")))); + assertThat(tariffMap.get(Instant.parse("2023-01-28T23:00:00Z")), is(equalTo(new BigDecimal("0.2581")))); + assertThat(tariffMap.get(Instant.parse("2023-01-29T05:00:00Z")), is(equalTo(new BigDecimal("0.7742")))); + assertThat(tariffMap.get(Instant.parse("2023-01-29T16:00:00Z")), is(equalTo(new BigDecimal("2.3227")))); + assertThat(tariffMap.get(Instant.parse("2023-01-29T20:00:00Z")), is(equalTo(new BigDecimal("0.7742")))); + } + + @Test + void toHourlySystemTariff() throws IOException { + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(Instant.parse("2023-06-30T21:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistElectricityTax.json", + DatahubPricelistRecords.class); + Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); + + assertThat(tariffMap.size(), is(39)); + assertThat(tariffMap.get(Instant.parse("2023-06-30T21:00:00Z")), is(equalTo(new BigDecimal("0.008")))); + assertThat(tariffMap.get(Instant.parse("2023-06-30T22:00:00Z")), is(equalTo(new BigDecimal("0.697")))); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/action/EnergiDataServiceActionsTest.java new file mode 100644 index 0000000000000..fa47e09a03c9e --- /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(48)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.432225")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.05619")))); + } + + @Test + void getPricesSystemTariff() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("SystemTariff"); + assertThat(actual.size(), is(48)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.054")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.054")))); + } + + @Test + void getPricesElectricityTax() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("ElectricityTax"); + assertThat(actual.size(), is(48)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.008")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.008")))); + } + + @Test + void getPricesTransmissionNetTariff() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("TransmissionNetTariff"); + assertThat(actual.size(), is(48)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.058")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.058")))); + } + + @Test + void getPricesSpotPriceNetTariff() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("SpotPrice,NetTariff"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.425065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.323870054")))); + } + + @Test + void getPricesSpotPriceNetTariffElectricityTax() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("SpotPrice,NetTariff,ElectricityTax"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.433065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.331870054")))); + } + + @Test + void getPricesTotal() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices(); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054")))); + } + + @Test + void getPricesTotalAllElements() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions + .getPrices("spotprice,nettariff,systemtariff,electricitytax,transmissionnettariff"); + assertThat(actual.size(), is(35)); + assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027")))); + assertThat(actual.get(Instant.parse("2023-02-04T15:00:00Z")), is(equalTo(new BigDecimal("1.708765039")))); + assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054")))); + } + + @Test + void getPricesInvalidPriceElement() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.getPrices("spotprice,nettarif"); + assertThat(actual.size(), is(0)); + } + + @Test + void getPricesMixedCurrencies() throws IOException { + mockCommonDatasets(actions); + when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_EUR); + + Map actual = actions.getPrices("spotprice,nettariff"); + assertThat(actual.size(), is(0)); + } + + /** + * Calculate price in period 15:30-16:30 (UTC) with consumption 150 W and the following total prices: + * 15:00:00: 1.708765039 + * 16:00:00: 2.443870054 + * + * Result = (1.708765039 / 2) + (2.443870054 / 2) * 0.150 + * + * @throws IOException + */ + @Test + void calculatePriceSimple() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:30:00Z"), + Instant.parse("2023-02-04T16:30:00Z"), new QuantityType<>(150, Units.WATT)); + assertThat(actual, is(equalTo(new BigDecimal("0.311447631975000000")))); // 0.3114476319750 + } + + /** + * Calculate price in period 15:00-17:00 (UTC) with consumption 1000 W and the following total prices: + * 15:00:00: 1.708765039 + * 16:00:00: 2.443870054 + * + * Result = 1.708765039 + 2.443870054 + * + * @throws IOException + */ + @Test + void calculatePriceFullHours() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:00:00Z"), + Instant.parse("2023-02-04T17:00:00Z"), new QuantityType<>(1, Units.KILOVAR)); + assertThat(actual, is(equalTo(new BigDecimal("4.152635093000000000")))); // 4.152635093 + } + + @Test + void calculatePriceOutOfRangeStart() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-03T23:59:00Z"), + Instant.parse("2023-02-04T12:30:00Z"), new QuantityType<>(1000, Units.WATT)); + assertThat(actual, is(equalTo(BigDecimal.ZERO))); + } + + @Test + void calculatePriceOutOfRangeEnd() throws IOException { + mockCommonDatasets(actions); + + BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-05T22:00:00Z"), + Instant.parse("2023-02-05T23:01:00Z"), new QuantityType<>(1000, Units.WATT)); + assertThat(actual, is(equalTo(BigDecimal.ZERO))); + } + + /** + * Miele G 6895 SCVi XXL K2O dishwasher, program ECO. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodWithPowerDishwasher() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4), + Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41), + Duration.ofMinutes(104)); + List> consumptions = List.of(QuantityType.valueOf(162.162162, Units.WATT), + QuantityType.valueOf(750, Units.WATT), QuantityType.valueOf(1500, Units.WATT), + QuantityType.valueOf(3000, Units.WATT), QuantityType.valueOf(1500, Units.WATT), + QuantityType.valueOf(166.666666, Units.WATT), QuantityType.valueOf(146.341463, Units.WATT), + QuantityType.valueOf(0, Units.WATT)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), durations, consumptions); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithPowerOutOfRange() throws IOException { + mockCommonDatasets(actions); + + List durations = List.of(Duration.ofMinutes(61)); + List> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"), + Instant.parse("2023-02-06T00:01:00Z"), durations, consumptions); + assertThat(actual.size(), is(equalTo(0))); + } + + /** + * Miele G 6895 SCVi XXL K2O dishwasher, program ECO. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodWithEnergyDishwasher() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4), + Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236), durations, + QuantityType.valueOf(0.1, Units.KILOWATT_HOUR)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithEnergyTotalDurationIsExactSum() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(120), durations, + QuantityType.valueOf(100, Units.WATT_HOUR)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("0.293540001200000000")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z")))); + } + + @Test + void calculateCheapestPeriodWithEnergyTotalDurationInvalid() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + List durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60)); + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(119), durations, + QuantityType.valueOf(0.1, Units.KILOWATT_HOUR)); + assertThat(actual.size(), is(equalTo(0))); + } + + /** + * Like {@link #calculateCheapestPeriodWithEnergyDishwasher} but with unknown consumption/timetable map. + * + * @throws IOException + */ + @Test + void calculateCheapestPeriodAssumingLinearUnknownConsumption() throws IOException { + mockCommonDatasets(actions, "SpotPrices20230205.json"); + + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"), + Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236)); + assertThat(actual.get("LowestPrice"), is(nullValue())); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z")))); + assertThat(actual.get("HighestPrice"), is(nullValue())); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z")))); + } + + @Test + void calculateCheapestPeriodForLinearPowerUsage() throws IOException { + mockCommonDatasets(actions); + + Map actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"), + Instant.parse("2023-02-05T23:00:00Z"), Duration.ofMinutes(61), QuantityType.valueOf(1000, Units.WATT)); + assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.323990859575000000")))); + assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T12:00:00Z")))); + assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("2.589061780353348000")))); + assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-04T17:00:00Z")))); + } + + private void mockCommonDatasets(EnergiDataServiceActions actions) throws IOException { + mockCommonDatasets(actions, "SpotPrices20230204.json"); + } + + private void mockCommonDatasets(EnergiDataServiceActions actions, String spotPricesFilename) throws IOException { + SpotPrice[] spotPriceRecords = getObjectFromJson(spotPricesFilename, SpotPrice[].class); + Map spotPrices = Arrays.stream(spotPriceRecords) + .collect(Collectors.toMap(SpotPrice::hourStart, SpotPrice::spotPrice)); + + PriceListParser priceListParser = new PriceListParser( + Clock.fixed(spotPriceRecords[0].hourStart, EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)); + DatahubPricelistRecords datahubRecords = getObjectFromJson("NetTariffs.json", DatahubPricelistRecords.class); + Map netTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class); + Map systemTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class); + Map electricityTaxes = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class); + Map transmissionNetTariffs = priceListParser + .toHourly(Arrays.stream(datahubRecords.records()).toList()); + + when(handler.getSpotPrices()).thenReturn(spotPrices); + when(handler.getNetTariffs()).thenReturn(netTariffs); + when(handler.getSystemTariffs()).thenReturn(systemTariffs); + when(handler.getElectricityTaxes()).thenReturn(electricityTaxes); + when(handler.getTransmissionNetTariffs()).thenReturn(transmissionNetTariffs); + when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_DKK); + actions.setThingHandler(handler); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java new file mode 100644 index 0000000000000..9334b3d903c47 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/DateQueryParameterTest.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.time.Duration; +import java.time.LocalDate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for {@link DateQueryParameter}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class DateQueryParameterTest { + + @Test + void dateQueryParameterTypeWithNegativeOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(-12)); + assertThat(parameter.toString(), is(equalTo("utcnow-PT12H"))); + } + + @Test + void dateQueryParameterTypeWithPositiveOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofHours(12)); + assertThat(parameter.toString(), is(equalTo("utcnow+PT12H"))); + } + + @Test + void dateQueryParameterTypeWithZeroOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ZERO); + assertThat(parameter.toString(), is(equalTo("utcnow"))); + } + + @Test + void dateQueryParameterTypeWithoutOffset() { + DateQueryParameter parameter = DateQueryParameter.of(DateQueryParameterType.NOW); + assertThat(parameter.toString(), is(equalTo("now"))); + } + + @Test + void localDate() { + DateQueryParameter parameter = DateQueryParameter.of(LocalDate.of(2023, 2, 28)); + assertThat(parameter.toString(), is(equalTo("2023-02-28"))); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java new file mode 100644 index 0000000000000..15e135e2c38a6 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/api/GlobalLocationNumberTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for {@link GlobalLocationNumber}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class GlobalLocationNumberTest { + + @Test + void isValid() { + assertThat(GlobalLocationNumber.of("5790000682102").isValid(), is(true)); + } + + @Test + void isInvalid() { + assertThat(GlobalLocationNumber.of("5790000682103").isValid(), is(false)); + } + + @Test + void emptyIsInvalid() { + assertThat(GlobalLocationNumber.EMPTY.isValid(), is(false)); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java new file mode 100644 index 0000000000000..90a6c43177a1f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/ExponentialBackoffTest.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link ExponentialBackoff}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ExponentialBackoffTest { + + @Test + void exponential() { + RetryStrategy retryPolicy = new ExponentialBackoff().withMinimum(Duration.ofSeconds(2)).withJitter(0.0); + for (long i = 2; i <= 256; i *= 2) { + assertThat(retryPolicy.getDuration().toSeconds(), is(i)); + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java new file mode 100644 index 0000000000000..22f4b21a75364 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/FixedTimeTest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link FixedTime}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class FixedTimeTest { + + @Test + void beforeNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T10:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ofHours(2))); + } + + @Test + void atNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T12:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ZERO)); + } + + @Test + void afterNoon() { + RetryStrategy retryPolicy = new FixedTime(LocalTime.of(12, 0), + Clock.fixed(Instant.parse("2023-01-24T13:00:00Z"), ZoneId.of("UTC"))); + assertThat(retryPolicy.getDuration(), is(Duration.ofHours(23))); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java new file mode 100644 index 0000000000000..4d8b6abe2a8a9 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/java/org/openhab/binding/energidataservice/internal/retry/strategy/LinearTest.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.retry.strategy; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; + +/** + * Tests for {@link Linear}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class LinearTest { + + @Test + void linear() { + RetryStrategy retryPolicy = new Linear().withMinimum(Duration.ofMinutes(1)).withJitter(0.0); + for (int i = 0; i <= 10; i++) { + assertThat(retryPolicy.getDuration(), is(Duration.ofMinutes(1))); + } + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json new file mode 100644 index 0000000000000..fce1f184d3c36 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistElectricityTax.json @@ -0,0 +1,83 @@ +{ + "total": 2, + "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-07-01T00:00:00", + "ValidTo": null, + "VATClass": "D02", + "Price1": 0.697, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + }, + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-07-01T00:00:00", + "VATClass": "D02", + "Price1": 0.008, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json new file mode 100644 index 0000000000000..8d265147a1fbc --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistN1.json @@ -0,0 +1,588 @@ +{ + "total": 20, + "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-04-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 0.432225, + "Price19": 0.432225, + "Price20": 0.432225, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + }, + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 1.05619, + "Price19": 1.05619, + "Price20": 1.05619, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + }, + { + "ValidFrom": "2022-11-01T00:00:00", + "ValidTo": "2023-01-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.407717, + "Price2": 0.407717, + "Price3": 0.407717, + "Price4": 0.407717, + "Price5": 0.407717, + "Price6": 0.407717, + "Price7": 0.407717, + "Price8": 0.407717, + "Price9": 0.407717, + "Price10": 0.407717, + "Price11": 0.407717, + "Price12": 0.407717, + "Price13": 0.407717, + "Price14": 0.407717, + "Price15": 0.407717, + "Price16": 0.407717, + "Price17": 0.407717, + "Price18": 1.015888, + "Price19": 1.015888, + "Price20": 1.015888, + "Price21": 0.407717, + "Price22": 0.407717, + "Price23": 0.407717, + "Price24": 0.407717 + }, + { + "ValidFrom": "2022-10-01T00:00:00", + "ValidTo": "2022-11-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.31535, + "Price2": 0.31535, + "Price3": 0.31535, + "Price4": 0.31535, + "Price5": 0.31535, + "Price6": 0.31535, + "Price7": 0.31535, + "Price8": 0.31535, + "Price9": 0.31535, + "Price10": 0.31535, + "Price11": 0.31535, + "Price12": 0.31535, + "Price13": 0.31535, + "Price14": 0.31535, + "Price15": 0.31535, + "Price16": 0.31535, + "Price17": 0.31535, + "Price18": 0.821619, + "Price19": 0.821619, + "Price20": 0.821619, + "Price21": 0.31535, + "Price22": 0.31535, + "Price23": 0.31535, + "Price24": 0.31535 + }, + { + "ValidFrom": "2022-08-01T00:00:00", + "ValidTo": "2022-10-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.31535, + "Price2": 0.31535, + "Price3": 0.31535, + "Price4": 0.31535, + "Price5": 0.31535, + "Price6": 0.31535, + "Price7": 0.31535, + "Price8": 0.31535, + "Price9": 0.31535, + "Price10": 0.31535, + "Price11": 0.31535, + "Price12": 0.31535, + "Price13": 0.31535, + "Price14": 0.31535, + "Price15": 0.31535, + "Price16": 0.31535, + "Price17": 0.31535, + "Price18": 0.31535, + "Price19": 0.31535, + "Price20": 0.31535, + "Price21": 0.31535, + "Price22": 0.31535, + "Price23": 0.31535, + "Price24": 0.31535 + }, + { + "ValidFrom": "2022-04-01T00:00:00", + "ValidTo": "2022-08-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.227969, + "Price2": 0.227969, + "Price3": 0.227969, + "Price4": 0.227969, + "Price5": 0.227969, + "Price6": 0.227969, + "Price7": 0.227969, + "Price8": 0.227969, + "Price9": 0.227969, + "Price10": 0.227969, + "Price11": 0.227969, + "Price12": 0.227969, + "Price13": 0.227969, + "Price14": 0.227969, + "Price15": 0.227969, + "Price16": 0.227969, + "Price17": 0.227969, + "Price18": 0.227969, + "Price19": 0.227969, + "Price20": 0.227969, + "Price21": 0.227969, + "Price22": 0.227969, + "Price23": 0.227969, + "Price24": 0.227969 + }, + { + "ValidFrom": "2022-01-01T00:00:00", + "ValidTo": "2022-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.183226, + "Price2": 0.183226, + "Price3": 0.183226, + "Price4": 0.183226, + "Price5": 0.183226, + "Price6": 0.183226, + "Price7": 0.183226, + "Price8": 0.183226, + "Price9": 0.183226, + "Price10": 0.183226, + "Price11": 0.183226, + "Price12": 0.183226, + "Price13": 0.183226, + "Price14": 0.183226, + "Price15": 0.183226, + "Price16": 0.183226, + "Price17": 0.183226, + "Price18": 0.543732, + "Price19": 0.543732, + "Price20": 0.543732, + "Price21": 0.183226, + "Price22": 0.183226, + "Price23": 0.183226, + "Price24": 0.183226 + }, + { + "ValidFrom": "2021-10-01T00:00:00", + "ValidTo": "2022-01-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.5448, + "Price19": 0.5448, + "Price20": 0.5448, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2021-04-01T00:00:00", + "ValidTo": "2021-10-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.1717, + "Price19": 0.1717, + "Price20": 0.1717, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2021-01-01T00:00:00", + "ValidTo": "2021-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.1717, + "Price2": 0.1717, + "Price3": 0.1717, + "Price4": 0.1717, + "Price5": 0.1717, + "Price6": 0.1717, + "Price7": 0.1717, + "Price8": 0.1717, + "Price9": 0.1717, + "Price10": 0.1717, + "Price11": 0.1717, + "Price12": 0.1717, + "Price13": 0.1717, + "Price14": 0.1717, + "Price15": 0.1717, + "Price16": 0.1717, + "Price17": 0.1717, + "Price18": 0.5448, + "Price19": 0.5448, + "Price20": 0.5448, + "Price21": 0.1717, + "Price22": 0.1717, + "Price23": 0.1717, + "Price24": 0.1717 + }, + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "CD R", + "Price1": 0.0, + "Price2": 0.0, + "Price3": 0.0, + "Price4": 0.0, + "Price5": 0.0, + "Price6": 0.0, + "Price7": 0.0, + "Price8": 0.0, + "Price9": 0.0, + "Price10": 0.0, + "Price11": 0.0, + "Price12": 0.0, + "Price13": 0.0, + "Price14": 0.0, + "Price15": 0.0, + "Price16": 0.0, + "Price17": 0.0, + "Price18": 0.0, + "Price19": 0.0, + "Price20": 0.0, + "Price21": 0.0, + "Price22": 0.0, + "Price23": 0.0, + "Price24": 0.0 + }, + { + "ValidFrom": "2022-12-01T00:00:00", + "ValidTo": "2023-01-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.407717, + "Price2": -0.407717, + "Price3": -0.407717, + "Price4": -0.407717, + "Price5": -0.407717, + "Price6": -0.407717, + "Price7": -0.407717, + "Price8": -0.407717, + "Price9": -0.407717, + "Price10": -0.407717, + "Price11": -0.407717, + "Price12": -0.407717, + "Price13": -0.407717, + "Price14": -0.407717, + "Price15": -0.407717, + "Price16": -0.407717, + "Price17": -0.407717, + "Price18": -1.015888, + "Price19": -1.015888, + "Price20": -1.015888, + "Price21": -0.407717, + "Price22": -0.407717, + "Price23": -0.407717, + "Price24": -0.407717 + }, + { + "ValidFrom": "2022-10-01T00:00:00", + "ValidTo": "2022-12-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.042484, + "Price19": -0.042484, + "Price20": -0.042484, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-08-01T00:00:00", + "ValidTo": "2022-10-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.0202, + "Price19": -0.0202, + "Price20": -0.0202, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-04-01T00:00:00", + "ValidTo": "2022-08-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.0202, + "Price19": -0.0202, + "Price20": -0.0202, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2022-01-01T00:00:00", + "ValidTo": "2022-04-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0202, + "Price2": -0.0202, + "Price3": -0.0202, + "Price4": -0.0202, + "Price5": -0.0202, + "Price6": -0.0202, + "Price7": -0.0202, + "Price8": -0.0202, + "Price9": -0.0202, + "Price10": -0.0202, + "Price11": -0.0202, + "Price12": -0.0202, + "Price13": -0.0202, + "Price14": -0.0202, + "Price15": -0.0202, + "Price16": -0.0202, + "Price17": -0.0202, + "Price18": -0.042484, + "Price19": -0.042484, + "Price20": -0.042484, + "Price21": -0.0202, + "Price22": -0.0202, + "Price23": -0.0202, + "Price24": -0.0202 + }, + { + "ValidFrom": "2021-10-01T00:00:00", + "ValidTo": "2022-01-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-04-01T00:00:00", + "ValidTo": "2021-10-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0224, + "Price19": -0.0224, + "Price20": -0.0224, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-03-01T00:00:00", + "ValidTo": "2021-04-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0224, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + }, + { + "ValidFrom": "2021-01-01T00:00:00", + "ValidTo": "2021-03-01T00:00:00", + "ChargeTypeCode": "CD R", + "Price1": -0.0224, + "Price2": -0.0224, + "Price3": -0.0224, + "Price4": -0.0224, + "Price5": -0.0224, + "Price6": -0.0224, + "Price7": -0.0224, + "Price8": -0.0224, + "Price9": -0.0224, + "Price10": -0.0224, + "Price11": -0.0224, + "Price12": -0.0224, + "Price13": -0.0224, + "Price14": -0.0224, + "Price15": -0.0224, + "Price16": -0.0224, + "Price17": -0.0471, + "Price18": -0.0471, + "Price19": -0.0471, + "Price20": -0.0471, + "Price21": -0.0224, + "Price22": -0.0224, + "Price23": -0.0224, + "Price24": -0.0224 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json new file mode 100644 index 0000000000000..7641d136c6d4d --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistNordEnergi.json @@ -0,0 +1,37 @@ +{ + "total": 1, + "filters": "{\"Note\":[\"Nettarif C\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000610877\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "TA031U200", + "Price1": 0.245, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json new file mode 100644 index 0000000000000..e0610a8caf8e8 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/DatahubPricelistTrefor.json @@ -0,0 +1,2908 @@ +{ + "total": 850, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000706686\"],\"ChargeTypeCode\":[\"46\"],\"Note\":[\"Nettarif C time\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-04-30T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-29T00:00:00", + "ValidTo": "2023-04-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-28T00:00:00", + "ValidTo": "2023-04-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-27T00:00:00", + "ValidTo": "2023-04-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-26T00:00:00", + "ValidTo": "2023-04-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-25T00:00:00", + "ValidTo": "2023-04-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-24T00:00:00", + "ValidTo": "2023-04-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-23T00:00:00", + "ValidTo": "2023-04-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-22T00:00:00", + "ValidTo": "2023-04-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-21T00:00:00", + "ValidTo": "2023-04-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-20T00:00:00", + "ValidTo": "2023-04-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-19T00:00:00", + "ValidTo": "2023-04-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-18T00:00:00", + "ValidTo": "2023-04-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-17T00:00:00", + "ValidTo": "2023-04-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-16T00:00:00", + "ValidTo": "2023-04-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-15T00:00:00", + "ValidTo": "2023-04-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-14T00:00:00", + "ValidTo": "2023-04-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-13T00:00:00", + "ValidTo": "2023-04-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-12T00:00:00", + "ValidTo": "2023-04-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-11T00:00:00", + "ValidTo": "2023-04-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-10T00:00:00", + "ValidTo": "2023-04-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-09T00:00:00", + "ValidTo": "2023-04-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-08T00:00:00", + "ValidTo": "2023-04-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-07T00:00:00", + "ValidTo": "2023-04-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-06T00:00:00", + "ValidTo": "2023-04-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-05T00:00:00", + "ValidTo": "2023-04-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-04T00:00:00", + "ValidTo": "2023-04-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-03T00:00:00", + "ValidTo": "2023-04-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-02T00:00:00", + "ValidTo": "2023-04-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-04-01T00:00:00", + "ValidTo": "2023-04-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.3871, + "Price8": 0.3871, + "Price9": 0.3871, + "Price10": 0.3871, + "Price11": 0.3871, + "Price12": 0.3871, + "Price13": 0.3871, + "Price14": 0.3871, + "Price15": 0.3871, + "Price16": 0.3871, + "Price17": 0.3871, + "Price18": 1.0065, + "Price19": 1.0065, + "Price20": 1.0065, + "Price21": 1.0065, + "Price22": 0.3871, + "Price23": 0.3871, + "Price24": 0.3871 + }, + { + "ValidFrom": "2023-03-31T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-30T00:00:00", + "ValidTo": "2023-03-31T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-29T00:00:00", + "ValidTo": "2023-03-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-28T00:00:00", + "ValidTo": "2023-03-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-27T00:00:00", + "ValidTo": "2023-03-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-26T00:00:00", + "ValidTo": "2023-03-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-25T00:00:00", + "ValidTo": "2023-03-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-24T00:00:00", + "ValidTo": "2023-03-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-23T00:00:00", + "ValidTo": "2023-03-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-22T00:00:00", + "ValidTo": "2023-03-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-21T00:00:00", + "ValidTo": "2023-03-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-20T00:00:00", + "ValidTo": "2023-03-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-19T00:00:00", + "ValidTo": "2023-03-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-18T00:00:00", + "ValidTo": "2023-03-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-17T00:00:00", + "ValidTo": "2023-03-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-16T00:00:00", + "ValidTo": "2023-03-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-15T00:00:00", + "ValidTo": "2023-03-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-14T00:00:00", + "ValidTo": "2023-03-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-13T00:00:00", + "ValidTo": "2023-03-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-12T00:00:00", + "ValidTo": "2023-03-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-11T00:00:00", + "ValidTo": "2023-03-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-10T00:00:00", + "ValidTo": "2023-03-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-09T00:00:00", + "ValidTo": "2023-03-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-08T00:00:00", + "ValidTo": "2023-03-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-07T00:00:00", + "ValidTo": "2023-03-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-06T00:00:00", + "ValidTo": "2023-03-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-05T00:00:00", + "ValidTo": "2023-03-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-04T00:00:00", + "ValidTo": "2023-03-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-03T00:00:00", + "ValidTo": "2023-03-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-02T00:00:00", + "ValidTo": "2023-03-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-03-01T00:00:00", + "ValidTo": "2023-03-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-28T00:00:00", + "ValidTo": "2023-03-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-27T00:00:00", + "ValidTo": "2023-02-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-26T00:00:00", + "ValidTo": "2023-02-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-25T00:00:00", + "ValidTo": "2023-02-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-24T00:00:00", + "ValidTo": "2023-02-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-23T00:00:00", + "ValidTo": "2023-02-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-22T00:00:00", + "ValidTo": "2023-02-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-21T00:00:00", + "ValidTo": "2023-02-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-20T00:00:00", + "ValidTo": "2023-02-21T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-19T00:00:00", + "ValidTo": "2023-02-20T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-18T00:00:00", + "ValidTo": "2023-02-19T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-17T00:00:00", + "ValidTo": "2023-02-18T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-16T00:00:00", + "ValidTo": "2023-02-17T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-15T00:00:00", + "ValidTo": "2023-02-16T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-14T00:00:00", + "ValidTo": "2023-02-15T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-13T00:00:00", + "ValidTo": "2023-02-14T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-12T00:00:00", + "ValidTo": "2023-02-13T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-11T00:00:00", + "ValidTo": "2023-02-12T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-10T00:00:00", + "ValidTo": "2023-02-11T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-09T00:00:00", + "ValidTo": "2023-02-10T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-08T00:00:00", + "ValidTo": "2023-02-09T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-07T00:00:00", + "ValidTo": "2023-02-08T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-06T00:00:00", + "ValidTo": "2023-02-07T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-05T00:00:00", + "ValidTo": "2023-02-06T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-04T00:00:00", + "ValidTo": "2023-02-05T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-03T00:00:00", + "ValidTo": "2023-02-04T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-02T00:00:00", + "ValidTo": "2023-02-03T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-02-01T00:00:00", + "ValidTo": "2023-02-02T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-31T00:00:00", + "ValidTo": "2023-02-01T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-30T00:00:00", + "ValidTo": "2023-01-31T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-29T00:00:00", + "ValidTo": "2023-01-30T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-28T00:00:00", + "ValidTo": "2023-01-29T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-27T00:00:00", + "ValidTo": "2023-01-28T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-26T00:00:00", + "ValidTo": "2023-01-27T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-25T00:00:00", + "ValidTo": "2023-01-26T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-24T00:00:00", + "ValidTo": "2023-01-25T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-23T00:00:00", + "ValidTo": "2023-01-24T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-22T00:00:00", + "ValidTo": "2023-01-23T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + }, + { + "ValidFrom": "2023-01-21T00:00:00", + "ValidTo": "2023-01-22T00:00:00", + "ChargeTypeCode": "46", + "Price1": 0.2581, + "Price2": 0.2581, + "Price3": 0.2581, + "Price4": 0.2581, + "Price5": 0.2581, + "Price6": 0.2581, + "Price7": 0.7742, + "Price8": 0.7742, + "Price9": 0.7742, + "Price10": 0.7742, + "Price11": 0.7742, + "Price12": 0.7742, + "Price13": 0.7742, + "Price14": 0.7742, + "Price15": 0.7742, + "Price16": 0.7742, + "Price17": 0.7742, + "Price18": 2.3227, + "Price19": 2.3227, + "Price20": 2.3227, + "Price21": 2.3227, + "Price22": 0.7742, + "Price23": 0.7742, + "Price24": 0.7742 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json new file mode 100644 index 0000000000000..5ee45ea673045 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/ElectricityTaxes.json @@ -0,0 +1,45 @@ +{ + "total": 1, + "filters": "{\"GLN_Number\":[\"5790000432752\"],\"ChargeType\":[\"D03\"],\"Note\":[\"Elafgift\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ChargeOwner": "Energinet Systemansvar A/S (SYO)", + "GLN_Number": "5790000432752", + "ChargeType": "D03", + "ChargeTypeCode": "EA-001", + "Note": "Elafgift", + "Description": "Elafgiften", + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-07-01T00:00:00", + "VATClass": "D02", + "Price1": 0.008, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null, + "TransparentInvoicing": 1, + "TaxIndicator": 1, + "ResolutionDuration": "P1D" + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json new file mode 100644 index 0000000000000..1af9754e1a6b0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/NetTariffs.json @@ -0,0 +1,37 @@ +{ + "total": 1, + "filters": "{\"ChargeTypeCode\":[\"CD\",\"CD R\"],\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790001089030\"]}", + "limit": 100, + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": "2023-04-01T00:00:00", + "ChargeTypeCode": "CD", + "Price1": 0.432225, + "Price2": 0.432225, + "Price3": 0.432225, + "Price4": 0.432225, + "Price5": 0.432225, + "Price6": 0.432225, + "Price7": 0.432225, + "Price8": 0.432225, + "Price9": 0.432225, + "Price10": 0.432225, + "Price11": 0.432225, + "Price12": 0.432225, + "Price13": 0.432225, + "Price14": 0.432225, + "Price15": 0.432225, + "Price16": 0.432225, + "Price17": 0.432225, + "Price18": 1.05619, + "Price19": 1.05619, + "Price20": 1.05619, + "Price21": 0.432225, + "Price22": 0.432225, + "Price23": 0.432225, + "Price24": 0.432225 + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json new file mode 100644 index 0000000000000..76423da41996e --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230204.json @@ -0,0 +1,142 @@ +[ + { + "hourStart": "2023-02-04T12:00:00Z", + "spotPrice": 0.992840027 + }, + { + "hourStart": "2023-02-04T13:00:00Z", + "spotPrice": 0.998200012 + }, + { + "hourStart": "2023-02-04T14:00:00Z", + "spotPrice": 1.054180054 + }, + { + "hourStart": "2023-02-04T15:00:00Z", + "spotPrice": 1.156540039 + }, + { + "hourStart": "2023-02-04T16:00:00Z", + "spotPrice": 1.267680054 + }, + { + "hourStart": "2023-02-04T17:00:00Z", + "spotPrice": 1.370939941 + }, + { + "hourStart": "2023-02-04T18:00:00Z", + "spotPrice": 1.339670044 + }, + { + "hourStart": "2023-02-04T19:00:00Z", + "spotPrice": 1.24973999 + }, + { + "hourStart": "2023-02-04T20:00:00Z", + "spotPrice": 1.177160034 + }, + { + "hourStart": "2023-02-04T21:00:00Z", + "spotPrice": 0.979809998 + }, + { + "hourStart": "2023-02-04T22:00:00Z", + "spotPrice": 0.804200012 + }, + { + "hourStart": "2023-02-04T23:00:00Z", + "spotPrice": 0.82826001 + }, + { + "hourStart": "2023-02-05T00:00:00Z", + "spotPrice": 0.777280029 + }, + { + "hourStart": "2023-02-05T01:00:00Z", + "spotPrice": 0.771549988 + }, + { + "hourStart": "2023-02-05T02:00:00Z", + "spotPrice": 0.757559998 + }, + { + "hourStart": "2023-02-05T03:00:00Z", + "spotPrice": 0.751599976 + }, + { + "hourStart": "2023-02-05T04:00:00Z", + "spotPrice": 0.76373999 + }, + { + "hourStart": "2023-02-05T05:00:00Z", + "spotPrice": 0.764700012 + }, + { + "hourStart": "2023-02-05T06:00:00Z", + "spotPrice": 0.784650024 + }, + { + "hourStart": "2023-02-05T07:00:00Z", + "spotPrice": 0.79551001 + }, + { + "hourStart": "2023-02-05T08:00:00Z", + "spotPrice": 0.805789978 + }, + { + "hourStart": "2023-02-05T09:00:00Z", + "spotPrice": 0.807789978 + }, + { + "hourStart": "2023-02-05T10:00:00Z", + "spotPrice": 0.796849976 + }, + { + "hourStart": "2023-02-05T11:00:00Z", + "spotPrice": 0.756289978 + }, + { + "hourStart": "2023-02-05T12:00:00Z", + "spotPrice": 0.749369995 + }, + { + "hourStart": "2023-02-05T13:00:00Z", + "spotPrice": 0.7915 + }, + { + "hourStart": "2023-02-05T14:00:00Z", + "spotPrice": 0.838830017 + }, + { + "hourStart": "2023-02-05T15:00:00Z", + "spotPrice": 0.892859985 + }, + { + "hourStart": "2023-02-05T16:00:00Z", + "spotPrice": 1.01997998 + }, + { + "hourStart": "2023-02-05T17:00:00Z", + "spotPrice": 0.99452002 + }, + { + "hourStart": "2023-02-05T18:00:00Z", + "spotPrice": 0.976140015 + }, + { + "hourStart": "2023-02-05T19:00:00Z", + "spotPrice": 0.923669983 + }, + { + "hourStart": "2023-02-05T20:00:00Z", + "spotPrice": 0.906700012 + }, + { + "hourStart": "2023-02-05T21:00:00Z", + "spotPrice": 0.931859985 + }, + { + "hourStart": "2023-02-05T22:00:00Z", + "spotPrice": 0.941159973 + } +] diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json new file mode 100644 index 0000000000000..ce12925c942eb --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SpotPrices20230205.json @@ -0,0 +1,142 @@ +[ + { + "hourStart": "2023-02-05T12:00:00Z", + "spotPrice": 0.749609985 + }, + { + "hourStart": "2023-02-05T13:00:00Z", + "spotPrice": 0.79173999 + }, + { + "hourStart": "2023-02-05T14:00:00Z", + "spotPrice": 0.839090027 + }, + { + "hourStart": "2023-02-05T15:00:00Z", + "spotPrice": 0.893140015 + }, + { + "hourStart": "2023-02-05T16:00:00Z", + "spotPrice": 1.020299988 + }, + { + "hourStart": "2023-02-05T17:00:00Z", + "spotPrice": 0.994840027 + }, + { + "hourStart": "2023-02-05T18:00:00Z", + "spotPrice": 0.976450012 + }, + { + "hourStart": "2023-02-05T19:00:00Z", + "spotPrice": 0.923960022 + }, + { + "hourStart": "2023-02-05T20:00:00Z", + "spotPrice": 0.90698999 + }, + { + "hourStart": "2023-02-05T21:00:00Z", + "spotPrice": 0.932150024 + }, + { + "hourStart": "2023-02-05T22:00:00Z", + "spotPrice": 0.941460022 + }, + { + "hourStart": "2023-02-05T23:00:00Z", + "spotPrice": 1.07947998 + }, + { + "hourStart": "2023-02-06T00:00:00Z", + "spotPrice": 1.070030029 + }, + { + "hourStart": "2023-02-06T01:00:00Z", + "spotPrice": 1.082540039 + }, + { + "hourStart": "2023-02-06T02:00:00Z", + "spotPrice": 1.057819946 + }, + { + "hourStart": "2023-02-06T03:00:00Z", + "spotPrice": 1.0430 + }, + { + "hourStart": "2023-02-06T04:00:00Z", + "spotPrice": 1.10873999 + }, + { + "hourStart": "2023-02-06T05:00:00Z", + "spotPrice": 1.307810059 + }, + { + "hourStart": "2023-02-06T06:00:00Z", + "spotPrice": 1.493780029 + }, + { + "hourStart": "2023-02-06T07:00:00Z", + "spotPrice": 1.588630005 + }, + { + "hourStart": "2023-02-06T08:00:00Z", + "spotPrice": 1.493780029 + }, + { + "hourStart": "2023-02-06T09:00:00Z", + "spotPrice": 1.377869995 + }, + { + "hourStart": "2023-02-06T10:00:00Z", + "spotPrice": 1.338859985 + }, + { + "hourStart": "2023-02-06T11:00:00Z", + "spotPrice": 1.256069946 + }, + { + "hourStart": "2023-02-06T12:00:00Z", + "spotPrice": 1.199790039 + }, + { + "hourStart": "2023-02-06T13:00:00Z", + "spotPrice": 1.220189941 + }, + { + "hourStart": "2023-02-06T14:00:00Z", + "spotPrice": 1.270589966 + }, + { + "hourStart": "2023-02-06T15:00:00Z", + "spotPrice": 1.353449951 + }, + { + "hourStart": "2023-02-06T16:00:00Z", + "spotPrice": 1.481050049 + }, + { + "hourStart": "2023-02-06T17:00:00Z", + "spotPrice": 1.589449951 + }, + { + "hourStart": "2023-02-06T18:00:00Z", + "spotPrice": 1.52898999 + }, + { + "hourStart": "2023-02-06T19:00:00Z", + "spotPrice": 1.386280029 + }, + { + "hourStart": "2023-02-06T20:00:00Z", + "spotPrice": 1.239400024 + }, + { + "hourStart": "2023-02-06T21:00:00Z", + "spotPrice": 1.135319946 + }, + { + "hourStart": "2023-02-06T22:00:00Z", + "spotPrice": 1.14648999 + } +] diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json new file mode 100644 index 0000000000000..8b1d42b98391f --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/SystemTariffs.json @@ -0,0 +1,36 @@ +{ + "total": 1, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Systemtarif\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "41000", + "Price1": 0.054, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json new file mode 100644 index 0000000000000..c2ed993403483 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/test/resources/org/openhab/binding/energidataservice/internal/action/TransmissionNetTariffs.json @@ -0,0 +1,36 @@ +{ + "total": 1, + "filters": "{\"ChargeType\":[\"D03\"],\"GLN_Number\":[\"5790000432752\"],\"Note\":[\"Transmissions nettarif\"]}", + "dataset": "DatahubPricelist", + "records": [ + { + "ValidFrom": "2023-01-01T00:00:00", + "ValidTo": null, + "ChargeTypeCode": "40000", + "Price1": 0.058, + "Price2": null, + "Price3": null, + "Price4": null, + "Price5": null, + "Price6": null, + "Price7": null, + "Price8": null, + "Price9": null, + "Price10": null, + "Price11": null, + "Price12": null, + "Price13": null, + "Price14": null, + "Price15": null, + "Price16": null, + "Price17": null, + "Price18": null, + "Price19": null, + "Price20": null, + "Price21": null, + "Price22": null, + "Price23": null, + "Price24": null + } + ] +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 4020bcde0491a..7ddb9934453d3 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -124,6 +124,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 From e0e0298a100c9e3f1db402eeb2a16d8085eefddb Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 18 Mar 2023 00:19:35 +0100 Subject: [PATCH 02/17] Remove Value-Added Tax Signed-off-by: Jacob Laursen --- .../README.md | 34 +++++-------- .../internal/CacheManager.java | 6 +-- .../config/DatahubPriceConfiguration.java | 2 +- .../internal/config/PriceConfiguration.java | 32 ------------- .../EnergiDataServiceHandlerFactory.java | 8 +--- .../handler/EnergiDataServiceHandler.java | 48 ++----------------- .../main/resources/OH-INF/config/config.xml | 11 ----- .../OH-INF/i18n/energidataservice.properties | 4 -- .../resources/OH-INF/thing/channel-types.xml | 1 - 9 files changed, 21 insertions(+), 125 deletions(-) delete mode 100644 bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index b49b2c8e58b15..4e01640c8cbef 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -58,13 +58,10 @@ This has the following advantages: #### Value-Added Tax -The channels `currentSpotPrice`, `currentNetTariff`, `currentSystemTariff`, `currentElectricityTax` and `currentTransmissionNetTariff` can be configured to include VAT with this configuration parameter: - -| Name | Type | Description | Default | Required | Advanced | -|-----------------|---------|----------------------------------------------|---------|----------|----------| -| includeVAT | boolean | Add VAT to amount based on regional settings | no | no | no | - -Please be aware that this channel configuration will affect all linked items. +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. #### Current Net Tariff @@ -132,7 +129,6 @@ The format of the `hourlyPrices` JSON array is as follows: Future spot prices for the next day are usually available around 13:00 CET and are fetched around that time. Historic prices older than 12 hours are removed from the JSON array each hour. -Channel configuration for "Include VAT" is ignored, i.e. VAT is excluded. ## Thing Actions @@ -322,26 +318,19 @@ var priceMap = actions.getPrices("SpotPrice,NetTariff"); ### Thing Configuration ```java -Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] { - Channels: - Number : electricity#currentSpotPrice [ includeVAT="true" ] - Number : electricity#currentNetTariff [ includeVAT="true" ] - Number : electricity#currentSystemTariff [ includeVAT="true" ] - Number : electricity#currentElectricityTax [ includeVAT="true" ] - Number : electricity#currentTransmissionNetTariff [ includeVAT="true" ] -} +Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] ``` ### Item Configuration ```java Group:Number:SUM CurrentTotalPrice "Current Total Price" -Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSpotPrice"} -Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentNetTariff"} -Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentSystemTariff"} -Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentElectricityTax"} -Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) {channel="energidataservice:service:energidataservice:electricity#currentTransmissionNetTariff"} -String HourlyPrices "Hourly Prices" {channel="energidataservice:service:energidataservice:electricity#hourlyPrices"} +Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentSpotPrice" [profile="transform:VAT"] } +Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentNetTariff" [profile="transform:VAT"] } +Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentSystemTariff" [profile="transform:VAT"] } +Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentElectricityTax" [profile="transform:VAT"] } +Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentTransmissionNetTariff" [profile="transform:VAT"] } +String HourlyPrices "Hourly Prices" { channel="energidataservice:service:energidataservice:electricity#hourlyPrices" } ``` ### Thing Actions Example @@ -404,5 +393,4 @@ durationPhases.add(Duration.ofMinutes(41)) durationPhases.add(Duration.ofMinutes(104)) var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh) - ``` diff --git a/bundles/org.openhab.binding.energidataservice/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 index d4673e1afa62e..089e1c55f83ca 100644 --- 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 @@ -43,9 +43,9 @@ @NonNullByDefault public class CacheManager { - public final static int NUMBER_OF_HISTORIC_HOURS = 12; - public final static int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS; - public final static int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS; + public static final int NUMBER_OF_HISTORIC_HOURS = 12; + 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(); 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 index d5dd072b0d0d0..8a59da81db44f 100644 --- 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 @@ -30,7 +30,7 @@ * @author Jacob Laursen - Initial contribution */ @NonNullByDefault -public class DatahubPriceConfiguration extends PriceConfiguration { +public class DatahubPriceConfiguration { /** * Comma-separated list of charge type codes, e.g. "CD,CD R". diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java deleted file mode 100644 index 10628ba174bd2..0000000000000 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/config/PriceConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.energidataservice.internal.config; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * The {@link PriceConfiguration} class defines common configuration parameters for price - * channels. - * - * @author Jacob Laursen - Initial contribution - */ -@NonNullByDefault -public class PriceConfiguration { - - public static final String INCLUDE_VAT = "includeVAT"; - - /** - * Add VAT to amount based on regional settings. - */ - public boolean includeVAT = false; -} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/factory/EnergiDataServiceHandlerFactory.java index 4212d80857d17..4f2f4ef23bd56 100644 --- 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 @@ -20,7 +20,6 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler; -import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Thing; @@ -46,16 +45,13 @@ public class EnergiDataServiceHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE); private final HttpClient httpClient; - private final LocaleProvider localeProvider; private final TimeZoneProvider timeZoneProvider; @Activate public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory, - final @Reference LocaleProvider localeProvider, final @Reference TimeZoneProvider timeZoneProvider, - ComponentContext componentContext) { + final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) { super.activate(componentContext); this.httpClient = httpClientFactory.getCommonHttpClient(); - this.localeProvider = localeProvider; this.timeZoneProvider = timeZoneProvider; } @@ -69,7 +65,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_SERVICE.equals(thingTypeUID)) { - return new EnergiDataServiceHandler(thing, httpClient, localeProvider, timeZoneProvider); + 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 index 6d8bf4e1dbead..3f8845c4a0f61 100644 --- 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 @@ -48,11 +48,9 @@ import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration; import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration; -import org.openhab.binding.energidataservice.internal.config.PriceConfiguration; import org.openhab.binding.energidataservice.internal.exception.DataServiceException; import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory; import org.openhab.binding.energidataservice.internal.retry.RetryStrategy; -import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; @@ -81,7 +79,6 @@ public class EnergiDataServiceHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class); - private final LocaleProvider localeProvider; private final TimeZoneProvider timeZoneProvider; private final ApiController apiController; private final CacheManager cacheManager; @@ -97,10 +94,8 @@ private record Price(String hourStart, BigDecimal spotPrice, String spotPriceCur @Nullable BigDecimal transmissionNetTariff) { } - public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, LocaleProvider localeProvider, - TimeZoneProvider timeZoneProvider) { + public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { super(thing); - this.localeProvider = localeProvider; this.timeZoneProvider = timeZoneProvider; this.apiController = new ApiController(httpClient, timeZoneProvider); this.cacheManager = new CacheManager(); @@ -354,50 +349,15 @@ private void updateCurrentSpotPrice() { if (!isLinked(CHANNEL_CURRENT_SPOT_PRICE)) { return; } - BigDecimal price = getVATAdjustedPrice(cacheManager.getSpotPrice(), CHANNEL_CURRENT_SPOT_PRICE); - updateState(CHANNEL_CURRENT_SPOT_PRICE, price != null ? new DecimalType(price) : UnDefType.UNDEF); + BigDecimal spotPrice = cacheManager.getSpotPrice(); + updateState(CHANNEL_CURRENT_SPOT_PRICE, spotPrice != null ? new DecimalType(spotPrice) : UnDefType.UNDEF); } private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) { if (!isLinked(channelId)) { return; } - BigDecimal price = getVATAdjustedPrice(tariff, channelId); - updateState(channelId, price != null ? new DecimalType(price) : UnDefType.UNDEF); - } - - private @Nullable BigDecimal getVATAdjustedPrice(@Nullable BigDecimal price, String channelId) { - if (price == null) { - return price; - } - Channel channel = getThing().getChannel(channelId); - if (channel == null) { - return price; - } - Object obj = channel.getConfiguration().get(PriceConfiguration.INCLUDE_VAT); - if (obj == null) { - return price; - } - Boolean includeVAT = (Boolean) obj; - if (includeVAT) { - return price.multiply(getVATPercentageFactor()); - } - return price; - } - - private BigDecimal getVATPercentageFactor() { - String country = localeProvider.getLocale().getCountry(); - switch (country) { - case "DK": - case "NO": - case "SE": - return new BigDecimal("1.25"); - case "DE": - return new BigDecimal("1.19"); - default: - logger.debug("No VAT rate for country {}", country); - return BigDecimal.ONE; - } + updateState(channelId, tariff != null ? new DecimalType(tariff) : UnDefType.UNDEF); } private void updateHourlyPrices() { 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 index be4a186f2c28d..95194619ac9d2 100644 --- 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 @@ -65,18 +65,7 @@ - - - - Add VAT to amount based on regional settings. - - - - - - Add VAT to amount based on regional settings. - Comma-separated list of charge type codes. 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 index 441f2b974257d..ebaebd63470ab 100644 --- 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 @@ -78,8 +78,6 @@ channel-type.energidataservice.spotPrice.description = Spot price. channel-type.config.energidataservice.datahub-price.chargeTypeCodes.label = Charge Type Code Filters channel-type.config.energidataservice.datahub-price.chargeTypeCodes.description = Comma-separated list of charge type codes. -channel-type.config.energidataservice.datahub-price.includeVAT.label = Include VAT -channel-type.config.energidataservice.datahub-price.includeVAT.description = Add VAT to amount based on regional settings. channel-type.config.energidataservice.datahub-price.notes.label = Note Filters channel-type.config.energidataservice.datahub-price.notes.description = Comma-separated list of notes. channel-type.config.energidataservice.datahub-price.start.label = Query Start Date @@ -87,8 +85,6 @@ channel-type.config.energidataservice.datahub-price.start.description = Query st channel-type.config.energidataservice.datahub-price.start.option.StartOfDay = Start of day channel-type.config.energidataservice.datahub-price.start.option.StartOfMonth = Start of month channel-type.config.energidataservice.datahub-price.start.option.StartOfYear = Start of year -channel-type.config.energidataservice.spot-price.includeVAT.label = Include VAT -channel-type.config.energidataservice.spot-price.includeVAT.description = Add VAT to amount based on regional settings. # thing status descriptions 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 index ffab302bd670e..faf902a675697 100644 --- 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 @@ -10,7 +10,6 @@ Spot price. Price - From 8b9e8c2a19a84f5e6fdbcf7dedd467d7bf11e40f Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Tue, 21 Mar 2023 22:01:04 +0100 Subject: [PATCH 03/17] Migrate naming convention Signed-off-by: Jacob Laursen --- .../README.md | 38 +++++++++---------- .../EnergiDataServiceBindingConstants.java | 12 +++--- .../OH-INF/i18n/energidataservice.properties | 32 ++++++++-------- .../resources/OH-INF/thing/channel-groups.xml | 12 +++--- .../resources/OH-INF/thing/channel-types.xml | 6 +-- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index 4e01640c8cbef..ae87d29756d32 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -39,14 +39,14 @@ To obtain the Global Location Number of your grid company: ### Channel Group `electricity` -| Channel | Type | Description | Advanced | -|------------------------------|--------|------------------------------------------------------------------------------------------------|----------| -| currentSpotPrice | Number | Spot price in DKK or EUR per kWh for current hour | no | -| currentNetTariff | Number | Net tariff in DKK per kWh for current hour. Only available when `gridCompanyGLN` is configured | no | -| currentSystemTariff | Number | System tariff in DKK per kWh for current hour | no | -| currentElectricityTax | Number | Electricity tax in DKK per kWh for current hour | no | -| currentTransmissionNetTariff | Number | Transmission net tariff in DKK per kWh for current hour | no | -| hourlyPrices | String | JSON array with hourly prices from 12 hours ago and onward | yes | +| Channel | Type | Description | Advanced | +|---------------------------------|--------|------------------------------------------------------------------------------------------------|----------| +| current-spot-price | Number | Spot price in DKK or EUR per kWh for current hour | no | +| current-net-tariff | Number | Net tariff in DKK per kWh for current hour. Only available when `gridCompanyGLN` is configured | no | +| current-system-tariff | Number | System tariff in DKK per kWh for current hour | no | +| current-electricity-tax | Number | Electricity tax in DKK per kWh for current hour | no | +| current-transmission-net-tariff | Number | Transmission net tariff in DKK per kWh for current hour | 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. @@ -65,11 +65,11 @@ Once installed, simply select "Value-Added Tax" as Profile when linking an item. #### Current Net Tariff -Discounts are automatically taken into account for channel `currentNetTariff` so that it represents the actual price. +Discounts are automatically taken into account for channel `current-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 `currentNetTariff` has the following configuration parameters: +To override filters, the channel `current-net-tariff` has the following configuration parameters: | Name | Type | Description | Default | Required | Advanced | |-----------------|---------|----------------------------------------------------------------------------------------------------------------------------|---------|----------|----------| @@ -102,7 +102,7 @@ _Nord Energi Net:_ #### Hourly Prices -The format of the `hourlyPrices` JSON array is as follows: +The format of the `hourly-prices` JSON array is as follows: ```json [ @@ -132,14 +132,14 @@ 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 [hourlyPrices](#hourly-prices) channel. +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 indivial channels to items, or by linking the `hourlyPrices` channel to an item. +This potential problem can be prevented by linking the indivial channels to items, or by linking the `hourly-prices` channel to an item. ### `calculateCheapestPeriod` @@ -325,12 +325,12 @@ Thing energidataservice:service:energidataservice "Energi Data Service" [ priceA ```java Group:Number:SUM CurrentTotalPrice "Current Total Price" -Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentSpotPrice" [profile="transform:VAT"] } -Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentNetTariff" [profile="transform:VAT"] } -Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentSystemTariff" [profile="transform:VAT"] } -Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentElectricityTax" [profile="transform:VAT"] } -Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#currentTransmissionNetTariff" [profile="transform:VAT"] } -String HourlyPrices "Hourly Prices" { channel="energidataservice:service:energidataservice:electricity#hourlyPrices" } +Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-spot-price" [profile="transform:VAT"] } +Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-net-tariff" [profile="transform:VAT"] } +Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-system-tariff" [profile="transform:VAT"] } +Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-electricity-tax" [profile="transform:VAT"] } +Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-transmission-net-tariff" [profile="transform:VAT"] } +String HourlyPrices "Hourly Prices" { channel="energidataservice:service:energidataservice:electricity#hourly-prices" } ``` ### Thing Actions Example 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 index 38358f0c03343..482c70ff6d37d 100644 --- 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 @@ -41,17 +41,17 @@ public class EnergiDataServiceBindingConstants { // List of all Channel ids public static final String CHANNEL_CURRENT_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSpotPrice"; + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-spot-price"; public static final String CHANNEL_CURRENT_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentNetTariff"; + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-net-tariff"; public static final String CHANNEL_CURRENT_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentSystemTariff"; + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-system-tariff"; public static final String CHANNEL_CURRENT_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentElectricityTax"; + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-electricity-tax"; public static final String CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "currentTransmissionNetTariff"; + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-transmission-net-tariff"; public static final String CHANNEL_HOURLY_PRICES = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR - + "hourlyPrices"; + + "hourly-prices"; public static final Set ELECTRICITY_CHANNELS = Set.of(CHANNEL_CURRENT_SPOT_PRICE, CHANNEL_CURRENT_NET_TARIFF, CHANNEL_CURRENT_SYSTEM_TARIFF, CHANNEL_CURRENT_ELECTRICITY_TAX, 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 index ebaebd63470ab..5ab01b677a135 100644 --- 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 @@ -54,25 +54,25 @@ thing-type.config.energidataservice.service.priceArea.option.DK2 = East of the G channel-group-type.energidataservice.electricity.label = Electricity channel-group-type.energidataservice.electricity.description = Channels related to electricity -channel-group-type.energidataservice.electricity.channel.currentElectricityTax.label = Current Electricity Tax -channel-group-type.energidataservice.electricity.channel.currentElectricityTax.description = Electricity Tax in DKK per kWh for current hour. -channel-group-type.energidataservice.electricity.channel.currentNetTariff.label = Current Net Tariff -channel-group-type.energidataservice.electricity.channel.currentNetTariff.description = Net tariff in DKK per kWh for current hour. -channel-group-type.energidataservice.electricity.channel.currentSpotPrice.label = Current Spot Price -channel-group-type.energidataservice.electricity.channel.currentSpotPrice.description = Spot price in DKK or EUR per kWh for current hour. -channel-group-type.energidataservice.electricity.channel.currentSystemTariff.label = Current System Tariff -channel-group-type.energidataservice.electricity.channel.currentSystemTariff.description = System tariff in DKK per kWh for current hour. -channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.label = Current Transmission Tariff -channel-group-type.energidataservice.electricity.channel.currentTransmissionNetTariff.description = Transmission Net Tariff in DKK per kWh for current hour. +channel-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. # channel types -channel-type.energidataservice.datahubPrice.label = Datahub Price -channel-type.energidataservice.datahubPrice.description = Datahub price. -channel-type.energidataservice.hourlyPrices.label = Hourly Prices -channel-type.energidataservice.hourlyPrices.description = JSON array with hourly prices from 12 hours ago and onward. -channel-type.energidataservice.spotPrice.label = Spot Price -channel-type.energidataservice.spotPrice.description = Spot price. +channel-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 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 index e365653e0022e..3ab2375a5f953 100644 --- 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 @@ -8,27 +8,27 @@ Channels related to electricity - + Spot price in DKK or EUR per kWh for current hour. - + Net tariff in DKK per kWh for current hour. - + System tariff in DKK per kWh for current hour. - + Electricity Tax in DKK per kWh for current hour. - + Transmission Net Tariff in DKK per kWh for current hour. - + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml index faf902a675697..12d0273c708bd 100644 --- 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 @@ -4,7 +4,7 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + Number Spot price. @@ -12,7 +12,7 @@ - + Number Datahub price. @@ -21,7 +21,7 @@ - + String JSON array with hourly prices from 12 hours ago and onward. From 7a2fa81a82e0e1e8c86e943726c316d9c11fabf1 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 27 Mar 2023 17:59:05 +0200 Subject: [PATCH 04/17] Add channel configuration example Signed-off-by: Jacob Laursen --- bundles/org.openhab.binding.energidataservice/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index ae87d29756d32..ff6da1f4dc465 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -318,7 +318,10 @@ var priceMap = actions.getPrices("SpotPrice,NetTariff"); ### Thing Configuration ```java -Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] +Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] { + Channels: + Number : electricity#current-net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ] +} ``` ### Item Configuration From 0d8797fbb1d7b5a184574cb7e168b96dff630910 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 9 Apr 2023 22:10:58 +0200 Subject: [PATCH 05/17] Remove current prefixes for forward compatibility with timestamped items Signed-off-by: Jacob Laursen --- .../README.md | 36 +++++++++---------- .../EnergiDataServiceBindingConstants.java | 25 +++++++------ .../handler/EnergiDataServiceHandler.java | 26 +++++++------- .../OH-INF/i18n/energidataservice.properties | 33 +++++++++++------ .../resources/OH-INF/thing/channel-groups.xml | 30 ++++++++-------- 5 files changed, 81 insertions(+), 69 deletions(-) diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index ff6da1f4dc465..75c42747e02b2 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -39,14 +39,14 @@ To obtain the Global Location Number of your grid company: ### Channel Group `electricity` -| Channel | Type | Description | Advanced | -|---------------------------------|--------|------------------------------------------------------------------------------------------------|----------| -| current-spot-price | Number | Spot price in DKK or EUR per kWh for current hour | no | -| current-net-tariff | Number | Net tariff in DKK per kWh for current hour. Only available when `gridCompanyGLN` is configured | no | -| current-system-tariff | Number | System tariff in DKK per kWh for current hour | no | -| current-electricity-tax | Number | Electricity tax in DKK per kWh for current hour | no | -| current-transmission-net-tariff | Number | Transmission net tariff in DKK per kWh for current hour | no | -| hourly-prices | String | JSON array with hourly prices from 12 hours ago and onward | yes | +| 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. @@ -63,13 +63,13 @@ To include VAT for items linked to the `Number` channels, the [VAT profile](http This must be installed separately. Once installed, simply select "Value-Added Tax" as Profile when linking an item. -#### Current Net Tariff +#### Net Tariff -Discounts are automatically taken into account for channel `current-net-tariff` so that it represents the actual price. +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 `current-net-tariff` has the following configuration parameters: +To override filters, the channel `net-tariff` has the following configuration parameters: | Name | Type | Description | Default | Required | Advanced | |-----------------|---------|----------------------------------------------------------------------------------------------------------------------------|---------|----------|----------| @@ -320,19 +320,19 @@ var priceMap = actions.getPrices("SpotPrice,NetTariff"); ```java Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] { Channels: - Number : electricity#current-net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ] + Number : electricity#net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ] } ``` ### Item Configuration ```java -Group:Number:SUM CurrentTotalPrice "Current Total Price" -Number CurrentSpotPrice "Current Spot Price" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-spot-price" [profile="transform:VAT"] } -Number CurrentNetTariff "Current Net Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-net-tariff" [profile="transform:VAT"] } -Number CurrentSystemTariff "Current System Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-system-tariff" [profile="transform:VAT"] } -Number CurrentElectricityTax "Current Electricity Tax" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-electricity-tax" [profile="transform:VAT"] } -Number CurrentTransmissionNetTariff "Current Transmission Tariff" (CurrentTotalPrice) { channel="energidataservice:service:energidataservice:electricity#current-transmission-net-tariff" [profile="transform:VAT"] } +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" } ``` 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 index 482c70ff6d37d..aa828627a98f0 100644 --- 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 @@ -40,22 +40,21 @@ public class EnergiDataServiceBindingConstants { public static final String CHANNEL_GROUP_ELECTRICITY = "electricity"; // List of all Channel ids - public static final String CHANNEL_CURRENT_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-spot-price"; - public static final String CHANNEL_CURRENT_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-net-tariff"; - public static final String CHANNEL_CURRENT_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-system-tariff"; - public static final String CHANNEL_CURRENT_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-electricity-tax"; - public static final String CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY - + ChannelUID.CHANNEL_GROUP_SEPARATOR + "current-transmission-net-tariff"; + 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_CURRENT_SPOT_PRICE, - CHANNEL_CURRENT_NET_TARIFF, CHANNEL_CURRENT_SYSTEM_TARIFF, CHANNEL_CURRENT_ELECTRICITY_TAX, - CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, CHANNEL_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"; 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 index 3f8845c4a0f61..8b174c66e539f 100644 --- 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 @@ -166,30 +166,30 @@ public Collection> getServices() { private void refreshElectricityPrices() { RetryStrategy retryPolicy; try { - if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { downloadSpotPrices(); } - if (isLinked(CHANNEL_CURRENT_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { downloadNetTariffs(); } - if (isLinked(CHANNEL_CURRENT_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { downloadSystemTariffs(); } - if (isLinked(CHANNEL_CURRENT_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) { downloadElectricityTaxes(); } - if (isLinked(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) { downloadTransmissionNetTariffs(); } updateStatus(ThingStatus.ONLINE); updatePrices(); - if (isLinked(CHANNEL_CURRENT_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { + if (isLinked(CHANNEL_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) { if (cacheManager.getNumberOfFutureSpotPrices() < 13) { retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE); @@ -303,7 +303,7 @@ private Collection downloadPriceLists(GlobalLocationNumb } private DatahubTariffFilter getNetTariffFilter() { - Channel channel = getThing().getChannel(CHANNEL_CURRENT_NET_TARIFF); + Channel channel = getThing().getChannel(CHANNEL_NET_TARIFF); if (channel == null) { return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN); } @@ -336,21 +336,21 @@ private void updatePrices() { cacheManager.cleanup(); updateCurrentSpotPrice(); - updateCurrentTariff(CHANNEL_CURRENT_NET_TARIFF, cacheManager.getNetTariff()); - updateCurrentTariff(CHANNEL_CURRENT_SYSTEM_TARIFF, cacheManager.getSystemTariff()); - updateCurrentTariff(CHANNEL_CURRENT_ELECTRICITY_TAX, cacheManager.getElectricityTax()); - updateCurrentTariff(CHANNEL_CURRENT_TRANSMISSION_NET_TARIFF, cacheManager.getTransmissionNetTariff()); + 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_CURRENT_SPOT_PRICE)) { + if (!isLinked(CHANNEL_SPOT_PRICE)) { return; } BigDecimal spotPrice = cacheManager.getSpotPrice(); - updateState(CHANNEL_CURRENT_SPOT_PRICE, spotPrice != null ? new DecimalType(spotPrice) : UnDefType.UNDEF); + updateState(CHANNEL_SPOT_PRICE, spotPrice != null ? new DecimalType(spotPrice) : UnDefType.UNDEF); } private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) { 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 index 5ab01b677a135..cddea56eff981 100644 --- 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 @@ -54,16 +54,16 @@ thing-type.config.energidataservice.service.priceArea.option.DK2 = East of the G channel-group-type.energidataservice.electricity.label = Electricity channel-group-type.energidataservice.electricity.description = Channels related to electricity -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. +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 @@ -86,6 +86,19 @@ channel-type.config.energidataservice.datahub-price.start.option.StartOfDay = St 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 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 index 3ab2375a5f953..2a22018260c37 100644 --- 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 @@ -8,25 +8,25 @@ Channels related to electricity - - - Spot price in DKK or EUR per kWh for current hour. + + + Current spot price in DKK or EUR per kWh. - - - Net tariff in DKK per kWh for current hour. + + + Current net tariff in DKK per kWh. - - - System tariff in DKK per kWh for current hour. + + + Current system tariff in DKK per kWh. - - - Electricity Tax in DKK per kWh for current hour. + + + Current electricity tax in DKK per kWh. - - - Transmission Net Tariff in DKK per kWh for current hour. + + + Current transmission net tariff in DKK per kWh. From 9376fc5f0791a9ed8a7ac08e2c1260660a036db8 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 10 Apr 2023 23:06:10 +0200 Subject: [PATCH 06/17] Add filter for another grid company Signed-off-by: Jacob Laursen --- .../internal/api/DatahubTariffFilterFactory.java | 5 +++++ .../src/main/resources/OH-INF/config/config.xml | 1 + .../main/resources/OH-INF/i18n/energidataservice.properties | 1 + 3 files changed, 7 insertions(+) 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 index 0d585ca282af3..deb9d67fc4df9 100644 --- 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 @@ -40,6 +40,7 @@ public class DatahubTariffFilterFactory { 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_N1_RANDERS = "5790000681372"; private static final String GLN_NETSELSKABET_ELVAERK = "5790000681075"; @@ -59,6 +60,7 @@ public class DatahubTariffFilterFactory { 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_SYSTEM_TARIFF = "Systemtarif"; private static final String NOTE_ELECTRICITY_TAX = "Elafgift"; private static final String NOTE_TRANSMISSION_NET_TARIFF = "Transmissions nettarif"; @@ -100,6 +102,9 @@ public static DatahubTariffFilter getNetTariffByGLN(String globalLocationNumber) 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)); 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 index 95194619ac9d2..6a7c81b6362f9 100644 --- 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 @@ -40,6 +40,7 @@ + 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 index cddea56eff981..e7a0b12674c17 100644 --- 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 @@ -30,6 +30,7 @@ thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610839 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.5790000681372 = N1 Randers thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681075 = Netselskabet Elværk From dbd09468a6483edfdcf3bfa1353e3fbc98dd33a9 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Wed, 12 Apr 2023 16:42:47 +0200 Subject: [PATCH 07/17] Use ISO 3166-1 alpha-2 codes in lowercase for XSD compliance Signed-off-by: Jacob Laursen --- .../src/main/resources/OH-INF/addon/addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c4a4a405e5586..0d407e1f1d2dc 100644 --- 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 @@ -7,5 +7,5 @@ Energi Data Service Binding This is the binding for Energi Data Service. cloud - DK,NO,SE + dk,no,se From a885b850feb3d3e388f1e676e156d53db9d08557 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Tue, 18 Apr 2023 20:45:34 +0200 Subject: [PATCH 08/17] Fix error handling for deserializers Signed-off-by: Jacob Laursen --- .../serialization/InstantDeserializer.java | 11 +++- .../serialization/LocalDateDeserializer.java | 7 ++- .../LocalDateTimeDeserializer.java | 7 ++- .../InstantDeserializerTest.java | 58 +++++++++++++++++++ .../LocalDateDeserializerTest.java | 56 ++++++++++++++++++ 5 files changed, 134 insertions(+), 5 deletions(-) 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 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 index 22c4ea66cbe3e..0591540f45233 100644 --- 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 @@ -14,6 +14,7 @@ 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; @@ -35,8 +36,12 @@ public class InstantDeserializer implements JsonDeserializer { public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) throws JsonParseException { String content = element.getAsString(); - // When writing this, the format of the provided UTC strings lacks the trailing 'Z'. - // In case this would be fixed in the future, gracefully support both with and without this. - return Instant.parse(content.endsWith("Z") ? content : content + "Z"); + 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 index 5b51db3d30b82..76f0b1fafe2c4 100644 --- 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 @@ -14,6 +14,7 @@ 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; @@ -34,6 +35,10 @@ public class LocalDateDeserializer implements JsonDeserializer { @Override public @Nullable LocalDate deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) throws JsonParseException { - return LocalDate.parse(element.getAsString().substring(0, 10)); + 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 index 47851c05e0df3..81b24587958e9 100644 --- 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 @@ -14,6 +14,7 @@ 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; @@ -34,6 +35,10 @@ public class LocalDateTimeDeserializer implements JsonDeserializer { + 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)))); + } +} From 7b599888c2173618c2c184f702079bc65578b382 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Thu, 27 Apr 2023 22:13:26 +0200 Subject: [PATCH 09/17] Fix compliance with RFC 9110 section 10.1.5 Signed-off-by: Jacob Laursen --- .../binding/energidataservice/internal/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c55282dd94efb..484986ff819d5 100644 --- 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 @@ -89,7 +89,7 @@ public class ApiController { public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) { this.httpClient = httpClient; this.timeZoneProvider = timeZoneProvider; - userAgent = "openHAB " + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); + userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); } /** From a47fe9dd7e1732a87e73400698c221cb0dc9c1fd Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Wed, 10 May 2023 22:33:19 +0200 Subject: [PATCH 10/17] Add JavaScript example code Signed-off-by: Jacob Laursen --- .../README.md | 93 +++++++++++++++++-- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index 75c42747e02b2..d39210d6df10a 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -77,7 +77,7 @@ To override filters, the channel `net-tariff` has the following configuration pa | notes | text | Comma-separated list of notes | | no | yes | | start | text | Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear | | no | yes | -The parameters `chargeTypeCodes` and `notes` are logically combined with "AND", so if only one parameter is needed for filter, only provide this parameter and leave the other one empty. +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. @@ -165,7 +165,7 @@ The result is a `Map` with the following keys: This is a convenience method that can be used when the power consumption is not known. The calculation will assume linear consumption and will find the best timeslot based on that. -The this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`. +For this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`. Example: @@ -338,15 +338,19 @@ String HourlyPrices "Hourly Prices" { channel="energidataservice:service ### 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"); +val actions = getActions("energidataservice", "energidataservice:service:energidataservice") -var priceMap = actions.getPrices(null); +var priceMap = actions.getPrices(null) var hourStart = now.toInstant().truncatedTo(ChronoUnit.HOURS) logInfo("Current total price excl. VAT", priceMap.get(hourStart).toString) @@ -377,10 +381,10 @@ consumptionPhases.add(146.341 | W) consumptionPhases.add(0 | W) var Map result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases) -logInfo("Lowest price", result.get("LowestPrice")) -logInfo("Cheapest start", result.get("CheapestStart")) -logInfo("Highest price price", result.get("HighestPrice")) -logInfo("Most expensive start", result.get("MostExpensiveStart")) +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 @@ -393,7 +397,76 @@ durationPhases.add(Duration.ofMinutes(2)) durationPhases.add(Duration.ofMinutes(4)) durationPhases.add(Duration.ofMinutes(36)) durationPhases.add(Duration.ofMinutes(41)) -durationPhases.add(Duration.ofMinutes(104)) -var Map result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh) +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")); ``` + +::: + +:::: From 3f73405fff8087a4e217a6995baa37b6dbc87119 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 20 May 2023 22:55:48 +0200 Subject: [PATCH 11/17] Refactor List to Collection and use iterators Signed-off-by: Jacob Laursen --- .../internal/PriceCalculator.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 index 3524f90b1ca9f..67e988909bc2f 100644 --- 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 @@ -18,7 +18,9 @@ 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; @@ -66,7 +68,7 @@ public PriceCalculator(Map priceMap) { * @return Map containing resulting values */ public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration, - List durationPhases, QuantityType energyUsedPerPhase) throws MissingPriceException { + Collection durationPhases, QuantityType energyUsedPerPhase) throws MissingPriceException { QuantityType energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR); if (energyInWattHour == null) { throw new IllegalArgumentException( @@ -119,7 +121,8 @@ public Map calculateCheapestPeriod(Instant earliestStart, Instan * @return Map containing resulting values */ public Map calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, - List durationPhases, List> consumptionPhases) throws MissingPriceException { + Collection durationPhases, Collection> consumptionPhases) + throws MissingPriceException { if (durationPhases.size() != consumptionPhases.size()) { throw new IllegalArgumentException("Number of phases do not match"); } @@ -137,8 +140,12 @@ public Map calculateCheapestPeriod(Instant earliestStart, Instan Duration minDurationUntilNextHour = Duration.ofHours(1); Instant atomStart = calculationStart; - int i = 0; - for (Duration atomDuration : durationPhases) { + 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); @@ -149,11 +156,9 @@ public Map calculateCheapestPeriod(Instant earliestStart, Instan minDurationUntilNextHour = durationUntilNextHour; } - BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, consumptionPhases.get(i)); + BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, atomConsumption); currentPrice = currentPrice.add(atomPrice); atomStart = atomEnd; - - i++; } if (currentPrice.compareTo(lowestPrice) < 0) { From 68b7487b8f69a9b3105c10caeb3f33331b306e6a Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Tue, 23 May 2023 20:50:37 +0200 Subject: [PATCH 12/17] Add filter for another grid company Signed-off-by: Jacob Laursen --- .../internal/api/DatahubTariffFilterFactory.java | 6 ++++++ .../src/main/resources/OH-INF/config/config.xml | 1 + .../main/resources/OH-INF/i18n/energidataservice.properties | 1 + 3 files changed, 8 insertions(+) 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 index deb9d67fc4df9..9910544cdd045 100644 --- 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 @@ -50,6 +50,7 @@ public class DatahubTariffFilterFactory { 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"; @@ -61,6 +62,7 @@ public class DatahubTariffFilterFactory { 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"; @@ -130,6 +132,10 @@ public static DatahubTariffFilter getNetTariffByGLN(String globalLocationNumber) 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: 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 index 6a7c81b6362f9..906048890ff15 100644 --- 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 @@ -50,6 +50,7 @@ + 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 index e7a0b12674c17..e977995277ffe 100644 --- 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 @@ -40,6 +40,7 @@ thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000395620 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 From 4d00b2270b5b51afa798448b027b616cb70f691c Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 29 May 2023 22:19:44 +0200 Subject: [PATCH 13/17] Extend cached history to 24 hours Signed-off-by: Jacob Laursen --- .../energidataservice/internal/CacheManager.java | 2 +- .../internal/CacheManagerTest.java | 8 ++++---- .../internal/PriceListParserTest.java | 16 ++++++++-------- .../action/EnergiDataServiceActionsTest.java | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) 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 index 089e1c55f83ca..d73ee12dd1d91 100644 --- 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 @@ -43,7 +43,7 @@ @NonNullByDefault public class CacheManager { - public static final int NUMBER_OF_HISTORIC_HOURS = 12; + 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; 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 index 5deecd5abd94c..a9016521198fd 100644 --- 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 @@ -40,7 +40,7 @@ public class CacheManagerTest { @Test void areSpotPricesFullyCachedToday() { Instant now = Instant.parse("2023-02-07T08:38:47Z"); - Instant first = Instant.parse("2023-02-06T20:00:00Z"); + Instant 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); @@ -73,7 +73,7 @@ void areSpotPricesFullyCachedTodayMissingAtEnd() { @Test void areSpotPricesFullyCachedTodayOtherTimezoneIsIgnored() { Instant now = Instant.parse("2023-02-07T08:38:47Z"); - Instant first = Instant.parse("2023-02-06T20:00:00Z"); + 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); @@ -84,7 +84,7 @@ void areSpotPricesFullyCachedTodayOtherTimezoneIsIgnored() { @Test void areSpotPricesFullyCachedTomorrow() { Instant now = Instant.parse("2023-02-07T12:00:00Z"); - Instant first = Instant.parse("2023-02-07T00: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); @@ -95,7 +95,7 @@ void areSpotPricesFullyCachedTomorrow() { @Test void areHistoricSpotPricesCached() { Instant now = Instant.parse("2023-02-07T08:38:47Z"); - Instant first = Instant.parse("2023-02-06T20:00:00Z"); + 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); 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 index 689966a500bd4..da0b31a78c950 100644 --- 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 @@ -71,7 +71,7 @@ void toHourlyNoChanges() throws IOException { DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(48)); + 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")))); @@ -85,7 +85,7 @@ void toHourlyNewTariffTomorrowWhenSummertime() throws IOException { DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(48)); + 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")))); @@ -99,7 +99,7 @@ void toHourlyNewTariffAtMidnight() throws IOException { DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD"); - assertThat(tariffMap.size(), is(48)); + 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")))); @@ -113,7 +113,7 @@ void toHourlyDiscount() throws IOException { Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD R"); - assertThat(tariffMap.size(), is(48)); + 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")))); @@ -126,7 +126,7 @@ void toHourlyTariffAndDiscountIsSum() throws IOException { DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(45)); + 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")))); } @@ -138,7 +138,7 @@ void toHourlyTariffAndDiscountIsFree() throws IOException { DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(48)); + 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")))); @@ -168,7 +168,7 @@ void toHourlyDailyTariffs() throws IOException { DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(56)); + 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")))); @@ -187,7 +187,7 @@ void toHourlySystemTariff() throws IOException { DatahubPricelistRecords.class); Map tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList()); - assertThat(tariffMap.size(), is(39)); + 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 index fa47e09a03c9e..3170973a0b0ec 100644 --- 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 @@ -112,7 +112,7 @@ void getPricesNetTariff() throws IOException { mockCommonDatasets(actions); Map actual = actions.getPrices("NetTariff"); - assertThat(actual.size(), is(48)); + 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")))); } @@ -122,7 +122,7 @@ void getPricesSystemTariff() throws IOException { mockCommonDatasets(actions); Map actual = actions.getPrices("SystemTariff"); - assertThat(actual.size(), is(48)); + 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")))); } @@ -132,7 +132,7 @@ void getPricesElectricityTax() throws IOException { mockCommonDatasets(actions); Map actual = actions.getPrices("ElectricityTax"); - assertThat(actual.size(), is(48)); + 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")))); } @@ -142,7 +142,7 @@ void getPricesTransmissionNetTariff() throws IOException { mockCommonDatasets(actions); Map actual = actions.getPrices("TransmissionNetTariff"); - assertThat(actual.size(), is(48)); + 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")))); } From eeddd5a513ef7c568c783e565d525c104c301615 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 19 Jun 2023 21:36:15 +0200 Subject: [PATCH 14/17] Remove filter for expired GLN Signed-off-by: Jacob Laursen --- .../internal/api/DatahubTariffFilterFactory.java | 4 ---- .../src/main/resources/OH-INF/config/config.xml | 1 - .../main/resources/OH-INF/i18n/energidataservice.properties | 1 - 3 files changed, 6 deletions(-) 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 index 9910544cdd045..2809c1592a585 100644 --- 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 @@ -42,7 +42,6 @@ public class DatahubTariffFilterFactory { 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_N1_RANDERS = "5790000681372"; private static final String GLN_NETSELSKABET_ELVAERK = "5790000681075"; private static final String GLN_NKE_ELNET = "5790001088231"; private static final String GLN_NORD_ENERGI_NET = "5790000610877"; @@ -110,9 +109,6 @@ public static DatahubTariffFilter getNetTariffByGLN(String globalLocationNumber) case GLN_N1: return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD"), ChargeTypeCode.of("CD R")), Set.of(), DateQueryParameter.of(N1_CUTOFF_DATE)); - case GLN_N1_RANDERS: - return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD")), Set.of(NOTE_NET_TARIFF_C), - DateQueryParameter.of(N1_CUTOFF_DATE)); case GLN_NETSELSKABET_ELVAERK: return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("0NCFF")), Set.of(NOTE_NET_TARIFF_C + " Flex"), DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1))); 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 index 906048890ff15..77d3dfe3c27c4 100644 --- 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 @@ -42,7 +42,6 @@ - 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 index e977995277ffe..7bd980264b839 100644 --- 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 @@ -32,7 +32,6 @@ thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000704842 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.5790000681372 = N1 Randers thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681075 = Netselskabet Elværk thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088231 = NKE-Elnet thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610877 = Nord Energi Net From a60efef867949b5a6ee9ea126993784a275b858a Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 2 Jul 2023 13:44:39 +0200 Subject: [PATCH 15/17] Fix typos Signed-off-by: Jacob Laursen --- bundles/org.openhab.binding.energidataservice/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index d39210d6df10a..41bcbe7dc251e 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -139,7 +139,7 @@ Actions use cached data for performing operations. Since data is only fetched when an item is linked to a channel, there might not be any cached data available. In this case the data will be fetched on demand and cached afterwards. The first action triggered on a given day may therefore be a bit slower, and is also prone to failing if the server call fails for any reason. -This potential problem can be prevented by linking the indivial channels to items, or by linking the `hourly-prices` channel to an item. +This potential problem can be prevented by linking the individual channels to items, or by linking the `hourly-prices` channel to an item. ### `calculateCheapestPeriod` @@ -182,7 +182,7 @@ var Map result = actions.calculateCheapestPeriod(now.toInstant() | duration | `Duration` | The duration to fit within the timeslot | | power | `QuantityType` | Linear power consumption | -This action is identical the the variant above, but with a known linear power consumption. +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: From 983efd69b0d73170ab6bd1e2f55ea74ae33d65ef Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 2 Jul 2023 13:48:47 +0200 Subject: [PATCH 16/17] Improve descriptions Signed-off-by: Jacob Laursen --- .../src/main/resources/OH-INF/addon/addon.xml | 2 +- .../main/resources/OH-INF/i18n/energidataservice.properties | 4 ++-- .../src/main/resources/OH-INF/thing/thing-service.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 index 0d407e1f1d2dc..70d43d22a6a23 100644 --- 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 @@ -5,7 +5,7 @@ binding Energi Data Service Binding - This is the binding for Energi Data Service. + 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/i18n/energidataservice.properties b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties index 7bd980264b839..8587e9c393ed3 100644 --- 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 @@ -1,12 +1,12 @@ # add-on addon.energidataservice.name = Energi Data Service Binding -addon.energidataservice.description = This is the binding for Energi Data Service. +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-type.energidataservice.service.description = This Thing represents the Energi Data Service API. # thing types config 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 index 84e459e170b3e..c00115a67ddd7 100644 --- 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 @@ -7,7 +7,7 @@ - This thing represents the Energi Data Service API. + This Thing represents the Energi Data Service API. From 14452dc8bdb82ebebd6671595e50ac0edc2751f4 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 2 Jul 2023 13:56:24 +0200 Subject: [PATCH 17/17] Improve logging Signed-off-by: Jacob Laursen --- .../handler/EnergiDataServiceHandler.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) 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 index 8b174c66e539f..16bffcf8da388 100644 --- 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 @@ -367,7 +367,7 @@ private void updateHourlyPrices() { Map spotPriceMap = cacheManager.getSpotPrices(); Price[] targetPrices = new Price[spotPriceMap.size()]; List> sourcePrices = spotPriceMap.entrySet().stream() - .sorted(Map.Entry. comparingByKey()).toList(); + .sorted(Map.Entry.comparingByKey()).toList(); int i = 0; for (Entry sourcePrice : sourcePrices) { @@ -401,7 +401,11 @@ public Map getSpotPrices() { try { downloadSpotPrices(); } catch (DataServiceException e) { - logger.warn("Error retrieving spot prices"); + 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(); } @@ -419,7 +423,11 @@ public Map getNetTariffs() { try { downloadNetTariffs(); } catch (DataServiceException e) { - logger.warn("Error retrieving net tariffs"); + 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(); } @@ -437,7 +445,11 @@ public Map getSystemTariffs() { try { downloadSystemTariffs(); } catch (DataServiceException e) { - logger.warn("Error retrieving system tariffs"); + 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(); } @@ -455,7 +467,11 @@ public Map getElectricityTaxes() { try { downloadElectricityTaxes(); } catch (DataServiceException e) { - logger.warn("Error retrieving electricity taxes"); + 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(); } @@ -473,7 +489,11 @@ public Map getTransmissionNetTariffs() { try { downloadTransmissionNetTariffs(); } catch (DataServiceException e) { - logger.warn("Error retrieving transmission net tariffs"); + 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(); }