mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[energidataservice] Initial contribution (#14376)
* Initial contribution Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Remove Value-Added Tax Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Migrate naming convention Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Add channel configuration example Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Remove current prefixes for forward compatibility with timestamped items Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Add filter for another grid company Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Use ISO 3166-1 alpha-2 codes in lowercase for XSD compliance Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Fix error handling for deserializers Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Fix compliance with RFC 9110 section 10.1.5 Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Add JavaScript example code Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Refactor List to Collection and use iterators Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Add filter for another grid company Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Extend cached history to 24 hours Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Remove filter for expired GLN Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Fix typos Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Improve descriptions Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> * Improve logging Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk> --------- Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
parent
4ddb3ce7e6
commit
6cfb1e295d
@ -92,6 +92,7 @@
|
||||
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
|
||||
/bundles/org.openhab.binding.elroconnects/ @mherwege
|
||||
/bundles/org.openhab.binding.energenie/ @hmerk
|
||||
/bundles/org.openhab.binding.energidataservice/ @jlaur
|
||||
/bundles/org.openhab.binding.enigma2/ @gdolfen
|
||||
/bundles/org.openhab.binding.enocean/ @fruggy83
|
||||
/bundles/org.openhab.binding.enphase/ @Hilbrand
|
||||
|
@ -456,6 +456,11 @@
|
||||
<artifactId>org.openhab.binding.energenie</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.energidataservice</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.enigma2</artifactId>
|
||||
|
13
bundles/org.openhab.binding.energidataservice/NOTICE
Normal file
13
bundles/org.openhab.binding.energidataservice/NOTICE
Normal file
@ -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
|
472
bundles/org.openhab.binding.energidataservice/README.md
Normal file
472
bundles/org.openhab.binding.energidataservice/README.md
Normal file
@ -0,0 +1,472 @@
|
||||
# Energi Data Service Binding
|
||||
|
||||
This binding integrates electricity prices from the Danish Energi Data Service ("Open energy data from Energinet to society").
|
||||
|
||||
This can be used to plan energy consumption, for example to calculate the cheapest period for running a dishwasher or charging an EV.
|
||||
|
||||
## Supported Things
|
||||
|
||||
All channels are available for thing type `service`.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### `service` Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required |
|
||||
|----------------|---------|---------------------------------------------------|---------------|----------|
|
||||
| priceArea | text | Price area for spot prices (same as bidding zone) | | yes |
|
||||
| currencyCode | text | Currency code in which to obtain spot prices | DKK | no |
|
||||
| gridCompanyGLN | integer | Global Location Number of the Grid Company | | no |
|
||||
| energinetGLN | integer | Global Location Number of Energinet | 5790000432752 | no |
|
||||
|
||||
#### Global Location Number of the Grid Company
|
||||
|
||||
The Global Location Number of your grid company can be selected from a built-in list of grid companies.
|
||||
To find the company in your area, you can go to [Find netselskab](https://greenpowerdenmark.dk/vejledning-teknik/nettilslutning/find-netselskab), enter your address, and the company will be shown.
|
||||
|
||||
If your company is not on the list, you can configure it manually.
|
||||
To obtain the Global Location Number of your grid company:
|
||||
|
||||
- Open a browser and go to [Eloverblik](https://eloverblik.dk/).
|
||||
- Click "Private customers" and log in with MitID (confirmation will appear as Energinet).
|
||||
- Click "Retrieve data" and select "Price data".
|
||||
- Open the file and look for the rows having **Price_type** = "Subscription".
|
||||
- In the columns **Name** and/or **Description** you should see the name of your grid company.
|
||||
- In column **Owner** you can find the GLN ("Global Location Number").
|
||||
- Most rows will have this **Owner**. If in doubt, try to look for rows __not__ having 5790000432752 as owner.
|
||||
|
||||
## Channels
|
||||
|
||||
### Channel Group `electricity`
|
||||
|
||||
| Channel | Type | Description | Advanced |
|
||||
|-------------------------|--------|---------------------------------------------------------------------------------------|----------|
|
||||
| spot-price | Number | Current spot price in DKK or EUR per kWh | no |
|
||||
| net-tariff | Number | Current net tariff in DKK per kWh. Only available when `gridCompanyGLN` is configured | no |
|
||||
| system-tariff | Number | Current system tariff in DKK per kWh | no |
|
||||
| electricity-tax | Number | Current electricity tax in DKK per kWh | no |
|
||||
| transmission-net-tariff | Number | Current transmission net tariff in DKK per kWh | no |
|
||||
| hourly-prices | String | JSON array with hourly prices from 12 hours ago and onward | yes |
|
||||
|
||||
_Please note:_ There is no channel providing the total price.
|
||||
Instead, create a group item with `SUM` as aggregate function and add the individual price items as children.
|
||||
This has the following advantages:
|
||||
|
||||
- Full customization possible: Freely choose the channels which should be included in the total.
|
||||
- An additional item containing the kWh fee from your electricity supplier can be added also.
|
||||
- Spot price can be configured in EUR while tariffs are in DKK.
|
||||
|
||||
#### Value-Added Tax
|
||||
|
||||
VAT is not included in any of the prices.
|
||||
To include VAT for items linked to the `Number` channels, the [VAT profile](https://www.openhab.org/addons/transformations/vat/) can be used.
|
||||
This must be installed separately.
|
||||
Once installed, simply select "Value-Added Tax" as Profile when linking an item.
|
||||
|
||||
#### Net Tariff
|
||||
|
||||
Discounts are automatically taken into account for channel `net-tariff` so that it represents the actual price.
|
||||
|
||||
The tariffs are downloaded using pre-configured filters for the different [Grid Company GLN's](#global-location-number-of-the-grid-company).
|
||||
If your company is not in the list, or the filters are not working, they can be manually overridden.
|
||||
To override filters, the channel `net-tariff` has the following configuration parameters:
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------|---------|----------------------------------------------------------------------------------------------------------------------------|---------|----------|----------|
|
||||
| chargeTypeCodes | text | Comma-separated list of charge type codes | | no | yes |
|
||||
| notes | text | Comma-separated list of notes | | no | yes |
|
||||
| start | text | Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear | | no | yes |
|
||||
|
||||
The parameters `chargeTypeCodes` and `notes` are logically combined with "AND", so if only one parameter is needed for the filter, only provide this parameter and leave the other one empty.
|
||||
Using any of these parameters will override the pre-configured filter entirely.
|
||||
|
||||
The parameter `start` can be used independently to override the query start date parameter.
|
||||
If used while leaving `chargeTypeCodes` and `notes` empty, only the date will be overridden.
|
||||
|
||||
Determining the right filters can be tricky, so if in doubt ask in the community forum.
|
||||
See also [Datahub Price List](https://www.energidataservice.dk/tso-electricity/DatahubPricelist).
|
||||
|
||||
##### Filter Examples
|
||||
|
||||
_N1:_
|
||||
| Parameter | Value |
|
||||
|-----------------|------------|
|
||||
| chargeTypeCodes | CD,CD R |
|
||||
| notes | |
|
||||
|
||||
_Nord Energi Net:_
|
||||
| Parameter | Value |
|
||||
|-----------------|------------|
|
||||
| chargeTypeCodes | TA031U200 |
|
||||
| notes | Nettarif C |
|
||||
|
||||
#### Hourly Prices
|
||||
|
||||
The format of the `hourly-prices` JSON array is as follows:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"hourStart": "2023-01-24T15:00:00Z",
|
||||
"spotPrice": 1.67076001,
|
||||
"spotPriceCurrency": "DKK",
|
||||
"netTariff": 0.432225,
|
||||
"systemTariff": 0.054000,
|
||||
"electricityTax": 0.008000,
|
||||
"transmissionNetTariff": 0.058000
|
||||
},
|
||||
{
|
||||
"hourStart": "2023-01-24T16:00:00Z",
|
||||
"spotPrice": 1.859880005,
|
||||
"spotPriceCurrency": "DKK",
|
||||
"netTariff": 1.05619,
|
||||
"systemTariff": 0.054000,
|
||||
"electricityTax": 0.008000,
|
||||
"transmissionNetTariff": 0.058000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Future spot prices for the next day are usually available around 13:00 CET and are fetched around that time.
|
||||
Historic prices older than 12 hours are removed from the JSON array each hour.
|
||||
|
||||
## Thing Actions
|
||||
|
||||
Thing actions can be used to perform calculations as well as import prices directly into rules without deserializing JSON from the [hourly-prices](#hourly-prices) channel.
|
||||
This is more convenient, much faster, and provides automatic summation of the price elements of interest.
|
||||
|
||||
Actions use cached data for performing operations.
|
||||
Since data is only fetched when an item is linked to a channel, there might not be any cached data available.
|
||||
In this case the data will be fetched on demand and cached afterwards.
|
||||
The first action triggered on a given day may therefore be a bit slower, and is also prone to failing if the server call fails for any reason.
|
||||
This potential problem can be prevented by linking the individual channels to items, or by linking the `hourly-prices` channel to an item.
|
||||
|
||||
### `calculateCheapestPeriod`
|
||||
|
||||
This action will determine the cheapest period for using energy.
|
||||
It comes in four variants with different input parameters.
|
||||
|
||||
The result is a `Map` with the following keys:
|
||||
|
||||
| Key | Type | Description |
|
||||
|--------------------|--------------|-------------------------------------------------------|
|
||||
| CheapestStart | `Instant` | Start time of cheapest calculated period |
|
||||
| LowestPrice | `BigDecimal` | The total price when starting at cheapest start |
|
||||
| MostExpensiveStart | `Instant` | Start time of most expensive calculated period |
|
||||
| HighestPrice | `BigDecimal` | The total price when starting at most expensive start |
|
||||
|
||||
#### `calculateCheapestPeriod` from Duration
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| earliestStart | `Instant` | Earliest start time allowed |
|
||||
| latestEnd | `Instant` | Latest end time allowed |
|
||||
| duration | `Duration` | The duration to fit within the timeslot |
|
||||
|
||||
This is a convenience method that can be used when the power consumption is not known.
|
||||
The calculation will assume linear consumption and will find the best timeslot based on that.
|
||||
For this reason the resulting `Map` will not contain the keys `LowestPrice` and `HighestPrice`.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90))
|
||||
```
|
||||
|
||||
#### `calculateCheapestPeriod` from Duration and Power
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| earliestStart | `Instant` | Earliest start time allowed |
|
||||
| latestEnd | `Instant` | Latest end time allowed |
|
||||
| duration | `Duration` | The duration to fit within the timeslot |
|
||||
| power | `QuantityType<Power>` | Linear power consumption |
|
||||
|
||||
This action is identical to the variant above, but with a known linear power consumption.
|
||||
As a result the price is also included in the result.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(90), 250 | W)
|
||||
```
|
||||
|
||||
#### `calculateCheapestPeriod` from Power Phases
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| earliestStart | `Instant` | Earliest start time allowed |
|
||||
| latestEnd | `Instant` | Latest end time allowed |
|
||||
| durationPhases | `List<Duration>` | List of durations for the phases |
|
||||
| powerPhases | `List<QuantityType<Power>>` | List of power consumption for each corresponding phase |
|
||||
|
||||
This variant is similar to the one above, but is based on a supplied timetable.
|
||||
|
||||
The timetable is supplied as two individual parameters, `durationPhases` and `powerPhases`, which must have the same size.
|
||||
This can be considered as different phases of using power, so each list member represents a period with a linear use of power.
|
||||
`durationPhases` should be a List populated by `Duration` objects, while `powerPhases` should be a List populated by `QuantityType<Power>` objects for that duration of time.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
val ArrayList<Duration> durationPhases = new ArrayList<Duration>()
|
||||
durationPhases.add(Duration.ofMinutes(37))
|
||||
durationPhases.add(Duration.ofMinutes(8))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(2))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(36))
|
||||
durationPhases.add(Duration.ofMinutes(41))
|
||||
durationPhases.add(Duration.ofMinutes(104))
|
||||
|
||||
val ArrayList<QuantityType<Power>> powerPhases = new ArrayList<QuantityType<Power>>()
|
||||
powerPhases.add(162.162 | W)
|
||||
powerPhases.add(750 | W)
|
||||
powerPhases.add(1500 | W)
|
||||
powerPhases.add(3000 | W)
|
||||
powerPhases.add(1500 | W)
|
||||
powerPhases.add(166.666 | W)
|
||||
powerPhases.add(146.341 | W)
|
||||
powerPhases.add(0 | W)
|
||||
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), durationPhases, powerPhases)
|
||||
```
|
||||
|
||||
Please note that the total duration will be calculated automatically as a sum of provided duration phases.
|
||||
Therefore, if the total duration is longer than the sum of phase durations, the remaining duration must be provided as last item with a corresponding 0 W power item.
|
||||
This is to ensure that the full program will finish before the provided `latestEnd`.
|
||||
|
||||
#### `calculateCheapestPeriod` from Energy per Phase
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| earliestStart | `Instant` | Earliest start time allowed |
|
||||
| latestEnd | `Instant` | Latest end time allowed |
|
||||
| totalDuration | `Duration` | The total duration of all phases |
|
||||
| durationPhases | `List<Duration>` | List of durations for the phases |
|
||||
| energyUsedPerPhase | `QuantityType<Energy>` | Fixed amount of energy used per phase |
|
||||
|
||||
This variant will assign the provided amount of energy into each phase.
|
||||
The use case for this variant is a simplification of the previous variant.
|
||||
For example, a dishwasher may provide energy consumption in 0.1 kWh steps.
|
||||
In this case it's a simple task to create a timetable accordingly without having to calculate the average power consumption per phase.
|
||||
Since a last phase may use no significant energy, the total duration must be provided also.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
val ArrayList<Duration> durationPhases = new ArrayList<Duration>()
|
||||
durationPhases.add(Duration.ofMinutes(37))
|
||||
durationPhases.add(Duration.ofMinutes(8))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(2))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(36))
|
||||
durationPhases.add(Duration.ofMinutes(41))
|
||||
|
||||
// 0.7 kWh is used in total (number of phases × energy used per phase)
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(12).toInstant(), Duration.ofMinutes(236), phases, 0.1 | kWh)
|
||||
```
|
||||
|
||||
### `calculatePrice`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| start | `Instant` | Start time |
|
||||
| end | `Instant` | End time |
|
||||
| power | `QuantityType<Power>` | Linear power consumption |
|
||||
|
||||
**Result:** Price as `BigDecimal`.
|
||||
|
||||
This action calculates the price for using given amount of power in the period from `start` till `end`.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
var price = actions.calculatePrice(now.toInstant(), now.plusHours(4).toInstant, 200 | W)
|
||||
```
|
||||
|
||||
### `getPrices`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------------|-----------------------------|--------------------------------------------------------|
|
||||
| priceElements | `String` | Comma-separated list of price elements to include |
|
||||
|
||||
**Result:** `Map<Instant, BigDecimal>`
|
||||
|
||||
The parameter `priceElements` is a case-insensitive comma-separated list of price elements to include in the returned hourly prices.
|
||||
These elements can be requested:
|
||||
|
||||
| Price element | Description |
|
||||
|-----------------------|-------------------------|
|
||||
| SpotPrice | Spot price |
|
||||
| NetTariff | Net tariff |
|
||||
| SystemTariff | System tariff |
|
||||
| ElectricityTax | Electricity tax |
|
||||
| TransmissionNetTariff | Transmission net tariff |
|
||||
|
||||
Using `null` as parameter returns the total prices including all price elements.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
var priceMap = actions.getPrices("SpotPrice,NetTariff");
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
### Thing Configuration
|
||||
|
||||
```java
|
||||
Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK1", currencyCode="DKK", gridCompanyGLN="5790001089030" ] {
|
||||
Channels:
|
||||
Number : electricity#net-tariff [ chargeTypeCodes="CD,CD R", start="StartOfYear" ]
|
||||
}
|
||||
```
|
||||
|
||||
### Item Configuration
|
||||
|
||||
```java
|
||||
Group:Number:SUM TotalPrice "Current Total Price" <price>
|
||||
Number SpotPrice "Current Spot Price" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#spot-price" [profile="transform:VAT"] }
|
||||
Number NetTariff "Current Net Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#net-tariff" [profile="transform:VAT"] }
|
||||
Number SystemTariff "Current System Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#system-tariff" [profile="transform:VAT"] }
|
||||
Number ElectricityTax "Current Electricity Tax" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#electricity-tax" [profile="transform:VAT"] }
|
||||
Number TransmissionNetTariff "Current Transmission Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#transmission-net-tariff" [profile="transform:VAT"] }
|
||||
String HourlyPrices "Hourly Prices" <price> { channel="energidataservice:service:energidataservice:electricity#hourly-prices" }
|
||||
```
|
||||
|
||||
### Thing Actions Example
|
||||
|
||||
:::: tabs
|
||||
|
||||
::: tab DSL
|
||||
|
||||
```javascript
|
||||
import java.time.Duration
|
||||
import java.util.ArrayList
|
||||
import java.util.Map
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
val actions = getActions("energidataservice", "energidataservice:service:energidataservice")
|
||||
|
||||
var priceMap = actions.getPrices(null)
|
||||
var hourStart = now.toInstant().truncatedTo(ChronoUnit.HOURS)
|
||||
logInfo("Current total price excl. VAT", priceMap.get(hourStart).toString)
|
||||
|
||||
var priceMap = actions.getPrices("SpotPrice,NetTariff");
|
||||
logInfo("Current spot price + net tariff excl. VAT", priceMap.get(hourStart).toString)
|
||||
|
||||
var price = actions.calculatePrice(Instant.now, now.plusHours(1).toInstant, 150 | W)
|
||||
logInfo("Total price for using 150 W for the next hour", price.toString)
|
||||
|
||||
val ArrayList<Duration> durationPhases = new ArrayList<Duration>()
|
||||
durationPhases.add(Duration.ofMinutes(37))
|
||||
durationPhases.add(Duration.ofMinutes(8))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(2))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(36))
|
||||
durationPhases.add(Duration.ofMinutes(41))
|
||||
durationPhases.add(Duration.ofMinutes(104))
|
||||
|
||||
val ArrayList<QuantityType<Power>> consumptionPhases = new ArrayList<QuantityType<Power>>()
|
||||
consumptionPhases.add(162.162 | W)
|
||||
consumptionPhases.add(750 | W)
|
||||
consumptionPhases.add(1500 | W)
|
||||
consumptionPhases.add(3000 | W)
|
||||
consumptionPhases.add(1500 | W)
|
||||
consumptionPhases.add(166.666 | W)
|
||||
consumptionPhases.add(146.341 | W)
|
||||
consumptionPhases.add(0 | W)
|
||||
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant, now.plusHours(24).toInstant, durationPhases, consumptionPhases)
|
||||
logInfo("Cheapest start", (result.get("CheapestStart") as Instant).toString)
|
||||
logInfo("Lowest price", (result.get("LowestPrice") as Number).doubleValue.toString)
|
||||
logInfo("Highest price", (result.get("HighestPrice") as Number).doubleValue.toString)
|
||||
logInfo("Most expensive start", (result.get("MostExpensiveStart") as Instant).toString)
|
||||
|
||||
// This is a simpler version taking advantage of the fact that each interval here represents 0.1 kWh of consumed energy.
|
||||
// In this example we have to provide the total duration to make sure we fit the latest end. This is because there is no
|
||||
// registered consumption in the last phase.
|
||||
val ArrayList<Duration> durationPhases = new ArrayList<Duration>()
|
||||
durationPhases.add(Duration.ofMinutes(37))
|
||||
durationPhases.add(Duration.ofMinutes(8))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(2))
|
||||
durationPhases.add(Duration.ofMinutes(4))
|
||||
durationPhases.add(Duration.ofMinutes(36))
|
||||
durationPhases.add(Duration.ofMinutes(41))
|
||||
|
||||
var Map<String, Object> result = actions.calculateCheapestPeriod(now.toInstant(), now.plusHours(24).toInstant(), Duration.ofMinutes(236), durationPhases, 0.1 | kWh)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: tab JavaScript
|
||||
|
||||
```javascript
|
||||
var edsActions = actions.get("energidataservice", "energidataservice:service:energidataservice");
|
||||
|
||||
// Get prices and convert to JavaScript Map with Instant string representation as keys.
|
||||
var priceMap = new Map();
|
||||
utils.javaMapToJsMap(edsActions.getPrices()).forEach((value, key) => {
|
||||
priceMap.set(key.toString(), value);
|
||||
});
|
||||
|
||||
var hourStart = time.Instant.now().truncatedTo(time.ChronoUnit.HOURS);
|
||||
console.log("Current total price excl. VAT: " + priceMap.get(hourStart.toString()));
|
||||
|
||||
utils.javaMapToJsMap(edsActions.getPrices("SpotPrice,NetTariff")).forEach((value, key) => {
|
||||
priceMap.set(key.toString(), value);
|
||||
});
|
||||
console.log("Current spot price + net tariff excl. VAT: " + priceMap.get(hourStart.toString()));
|
||||
|
||||
var price = edsActions.calculatePrice(time.Instant.now(), time.Instant.now().plusSeconds(3600), Quantity("150 W"));
|
||||
console.log("Total price for using 150 W for the next hour: " + price.toString());
|
||||
|
||||
var durationPhases = [];
|
||||
durationPhases.push(time.Duration.ofMinutes(37));
|
||||
durationPhases.push(time.Duration.ofMinutes(8));
|
||||
durationPhases.push(time.Duration.ofMinutes(4));
|
||||
durationPhases.push(time.Duration.ofMinutes(2));
|
||||
durationPhases.push(time.Duration.ofMinutes(4));
|
||||
durationPhases.push(time.Duration.ofMinutes(36));
|
||||
durationPhases.push(time.Duration.ofMinutes(41));
|
||||
durationPhases.push(time.Duration.ofMinutes(104));
|
||||
|
||||
var consumptionPhases = [];
|
||||
consumptionPhases.push(Quantity("162.162 W"));
|
||||
consumptionPhases.push(Quantity("750 W"));
|
||||
consumptionPhases.push(Quantity("1500 W"));
|
||||
consumptionPhases.push(Quantity("3000 W"));
|
||||
consumptionPhases.push(Quantity("1500 W"));
|
||||
consumptionPhases.push(Quantity("166.666 W"));
|
||||
consumptionPhases.push(Quantity("146.341 W"));
|
||||
consumptionPhases.push(Quantity("0 W"));
|
||||
|
||||
var result = edsActions.calculateCheapestPeriod(time.Instant.now(), time.Instant.now().plusSeconds(24*60*60), durationPhases, consumptionPhases);
|
||||
|
||||
console.log("Cheapest start: " + result.get("CheapestStart").toString());
|
||||
console.log("Lowest price: " + result.get("LowestPrice"));
|
||||
console.log("Highest price: " + result.get("HighestPrice"));
|
||||
console.log("Most expensive start: " + result.get("MostExpensiveStart").toString());
|
||||
|
||||
// This is a simpler version taking advantage of the fact that each interval here represents 0.1 kWh of consumed energy.
|
||||
// In this example we have to provide the total duration to make sure we fit the latest end. This is because there is no
|
||||
// registered consumption in the last phase.
|
||||
var durationPhases = [];
|
||||
durationPhases.push(time.Duration.ofMinutes(37));
|
||||
durationPhases.push(time.Duration.ofMinutes(8));
|
||||
durationPhases.push(time.Duration.ofMinutes(4));
|
||||
durationPhases.push(time.Duration.ofMinutes(2));
|
||||
durationPhases.push(time.Duration.ofMinutes(4));
|
||||
durationPhases.push(time.Duration.ofMinutes(36));
|
||||
durationPhases.push(time.Duration.ofMinutes(41));
|
||||
|
||||
var result = edsActions.calculateCheapestPeriod(time.Instant.now(), time.Instant.now().plusSeconds(24*60*60), time.Duration.ofMinutes(236), durationPhases, Quantity("0.1 kWh"));
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
25
bundles/org.openhab.binding.energidataservice/pom.xml
Normal file
25
bundles/org.openhab.binding.energidataservice/pom.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.energidataservice</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Energi Data Service Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.energidataservice-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-energidataservice" description="Energi Data Service Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.energidataservice/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Currency;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.energidataservice.internal.api.ChargeType;
|
||||
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
|
||||
import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
|
||||
import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords;
|
||||
import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
|
||||
import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
|
||||
import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link ApiController} is responsible for interacting with Energi Data Service.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ApiController {
|
||||
private static final String ENDPOINT = "https://api.energidataservice.dk/";
|
||||
private static final String DATASET_PATH = "dataset/";
|
||||
|
||||
private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices";
|
||||
private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist";
|
||||
|
||||
private static final String FILTER_KEY_PRICE_AREA = "PriceArea";
|
||||
private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType";
|
||||
private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode";
|
||||
private static final String FILTER_KEY_GLN_NUMBER = "GLN_Number";
|
||||
private static final String FILTER_KEY_NOTE = "Note";
|
||||
|
||||
private static final String HEADER_REMAINING_CALLS = "RemainingCalls";
|
||||
private static final String HEADER_TOTAL_CALLS = "TotalCalls";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ApiController.class);
|
||||
private final Gson gson = new GsonBuilder() //
|
||||
.registerTypeAdapter(Instant.class, new InstantDeserializer()) //
|
||||
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) //
|
||||
.create();
|
||||
private final HttpClient httpClient;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
private final String userAgent;
|
||||
|
||||
public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
|
||||
this.httpClient = httpClient;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve spot prices for requested area and in requested {@link Currency}.
|
||||
*
|
||||
* @param priceArea Usually DK1 or DK2
|
||||
* @param currency DKK or EUR
|
||||
* @param start Specifies the start point of the period for the data request
|
||||
* @param properties Map of properties which will be updated with metadata from headers
|
||||
* @return Records with pairs of hour start and price in requested currency.
|
||||
* @throws InterruptedException
|
||||
* @throws DataServiceException
|
||||
*/
|
||||
public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start,
|
||||
Map<String, String> properties) throws InterruptedException, DataServiceException {
|
||||
if (!SUPPORTED_CURRENCIES.contains(currency)) {
|
||||
throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
|
||||
}
|
||||
|
||||
Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES)
|
||||
.param("start", start.toString()) //
|
||||
.param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") //
|
||||
.param("columns", "HourUTC,SpotPrice" + currency) //
|
||||
.agent(userAgent) //
|
||||
.method(HttpMethod.GET);
|
||||
|
||||
logger.trace("GET request for {}", request.getURI());
|
||||
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
|
||||
updatePropertiesFromResponse(response, properties);
|
||||
|
||||
int status = response.getStatus();
|
||||
if (!HttpStatus.isSuccess(status)) {
|
||||
throw new DataServiceException("The request failed with HTTP error " + status, status);
|
||||
}
|
||||
String responseContent = response.getContentAsString();
|
||||
if (responseContent.isEmpty()) {
|
||||
throw new DataServiceException("Empty response");
|
||||
}
|
||||
logger.trace("Response content: '{}'", responseContent);
|
||||
|
||||
ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class);
|
||||
if (records == null) {
|
||||
throw new DataServiceException("Error parsing response");
|
||||
}
|
||||
|
||||
if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) {
|
||||
throw new DataServiceException("No records");
|
||||
}
|
||||
|
||||
return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(ElspotpriceRecord[]::new);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataServiceException("Error parsing response", e);
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new DataServiceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePropertiesFromResponse(ContentResponse response, Map<String, String> properties) {
|
||||
HttpFields headers = response.getHeaders();
|
||||
String remainingCalls = headers.get(HEADER_REMAINING_CALLS);
|
||||
if (remainingCalls != null) {
|
||||
properties.put(PROPERTY_REMAINING_CALLS, remainingCalls);
|
||||
}
|
||||
String totalCalls = headers.get(HEADER_TOTAL_CALLS);
|
||||
if (totalCalls != null) {
|
||||
properties.put(PROPERTY_TOTAL_CALLS, totalCalls);
|
||||
}
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
|
||||
properties.put(PROPERTY_LAST_CALL, LocalDateTime.now(timeZoneProvider.getTimeZone()).format(formatter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve datahub pricelists for requested GLN and charge type/charge type code.
|
||||
*
|
||||
* @param globalLocationNumber Global Location Number of the Charge Owner
|
||||
* @param chargeType Charge type (Subscription, Fee or Tariff).
|
||||
* @param tariffFilter Tariff filter (charge type codes and notes).
|
||||
* @param properties Map of properties which will be updated with metadata from headers
|
||||
* @return Price list for requested GLN and note.
|
||||
* @throws InterruptedException
|
||||
* @throws DataServiceException
|
||||
*/
|
||||
public Collection<DatahubPricelistRecord> getDatahubPriceLists(GlobalLocationNumber globalLocationNumber,
|
||||
ChargeType chargeType, DatahubTariffFilter tariffFilter, Map<String, String> properties)
|
||||
throws InterruptedException, DataServiceException {
|
||||
String columns = "ValidFrom,ValidTo,ChargeTypeCode";
|
||||
for (int i = 1; i < 25; i++) {
|
||||
columns += ",Price" + i;
|
||||
}
|
||||
|
||||
Map<String, Collection<String>> filterMap = new HashMap<>(Map.of( //
|
||||
FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), //
|
||||
FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString())));
|
||||
|
||||
Collection<String> chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings();
|
||||
if (!chargeTypeCodes.isEmpty()) {
|
||||
filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes);
|
||||
}
|
||||
|
||||
Collection<String> notes = tariffFilter.getNotes();
|
||||
if (!notes.isEmpty()) {
|
||||
filterMap.put(FILTER_KEY_NOTE, notes);
|
||||
}
|
||||
|
||||
Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST)
|
||||
.param("filter", mapToFilter(filterMap)) //
|
||||
.param("columns", columns) //
|
||||
.agent(userAgent) //
|
||||
.method(HttpMethod.GET);
|
||||
|
||||
DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter();
|
||||
if (!dateQueryParameter.isEmpty()) {
|
||||
request = request.param("start", dateQueryParameter.toString());
|
||||
}
|
||||
|
||||
logger.trace("GET request for {}", request.getURI());
|
||||
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
|
||||
updatePropertiesFromResponse(response, properties);
|
||||
|
||||
int status = response.getStatus();
|
||||
if (!HttpStatus.isSuccess(status)) {
|
||||
throw new DataServiceException("The request failed with HTTP error " + status, status);
|
||||
}
|
||||
String responseContent = response.getContentAsString();
|
||||
if (responseContent.isEmpty()) {
|
||||
throw new DataServiceException("Empty response");
|
||||
}
|
||||
logger.trace("Response content: '{}'", responseContent);
|
||||
|
||||
DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class);
|
||||
if (records == null) {
|
||||
throw new DataServiceException("Error parsing response");
|
||||
}
|
||||
|
||||
if (records.limit() > 0 && records.limit() < records.total()) {
|
||||
logger.warn("{} price list records available, but only {} returned.", records.total(), records.limit());
|
||||
}
|
||||
|
||||
if (Objects.isNull(records.records())) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return Arrays.stream(records.records()).filter(Objects::nonNull).toList();
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataServiceException("Error parsing response", e);
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new DataServiceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String mapToFilter(Map<String, Collection<String>> map) {
|
||||
return "{" + map.entrySet().stream().map(
|
||||
e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]")
|
||||
.collect(Collectors.joining(",")) + "}";
|
||||
}
|
||||
}
|
@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Currency;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
|
||||
|
||||
/**
|
||||
* The {@link CacheManager} is responsible for maintaining a cache of received
|
||||
* data from Energi Data Service.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CacheManager {
|
||||
|
||||
public static final int NUMBER_OF_HISTORIC_HOURS = 24;
|
||||
public static final int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS;
|
||||
public static final int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS;
|
||||
|
||||
private final Clock clock;
|
||||
private final PriceListParser priceListParser = new PriceListParser();
|
||||
|
||||
private Collection<DatahubPricelistRecord> netTariffRecords = new ArrayList<>();
|
||||
private Collection<DatahubPricelistRecord> systemTariffRecords = new ArrayList<>();
|
||||
private Collection<DatahubPricelistRecord> electricityTaxRecords = new ArrayList<>();
|
||||
private Collection<DatahubPricelistRecord> transmissionNetTariffRecords = new ArrayList<>();
|
||||
|
||||
private Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
|
||||
private Map<Instant, BigDecimal> netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
|
||||
private Map<Instant, BigDecimal> systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
|
||||
private Map<Instant, BigDecimal> electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
|
||||
private Map<Instant, BigDecimal> transmissionNetTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
|
||||
|
||||
public CacheManager() {
|
||||
this(Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
public CacheManager(Clock clock) {
|
||||
this.clock = clock.withZone(NORD_POOL_TIMEZONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data.
|
||||
*/
|
||||
public void clear() {
|
||||
netTariffRecords.clear();
|
||||
systemTariffRecords.clear();
|
||||
electricityTaxRecords.clear();
|
||||
transmissionNetTariffRecords.clear();
|
||||
|
||||
spotPriceMap.clear();
|
||||
netTariffMap.clear();
|
||||
systemTariffMap.clear();
|
||||
electricityTaxMap.clear();
|
||||
transmissionNetTariffMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert and cache the supplied {@link ElspotpriceRecord}s.
|
||||
*
|
||||
* @param records The records as received from Energi Data Service.
|
||||
* @param currency The currency in which the records were requested.
|
||||
*/
|
||||
public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) {
|
||||
boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency);
|
||||
for (ElspotpriceRecord record : records) {
|
||||
spotPriceMap.put(record.hour(),
|
||||
(isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000)));
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current "raw"/unprocessed net tariff records in cache.
|
||||
* Map of hourly tariffs will be updated automatically.
|
||||
*
|
||||
* @param records to cache
|
||||
*/
|
||||
public void putNetTariffs(Collection<DatahubPricelistRecord> records) {
|
||||
putDatahubRecords(netTariffRecords, records);
|
||||
updateNetTariffs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current "raw"/unprocessed system tariff records in cache.
|
||||
* Map of hourly tariffs will be updated automatically.
|
||||
*
|
||||
* @param records to cache
|
||||
*/
|
||||
public void putSystemTariffs(Collection<DatahubPricelistRecord> records) {
|
||||
putDatahubRecords(systemTariffRecords, records);
|
||||
updateSystemTariffs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current "raw"/unprocessed electricity tax records in cache.
|
||||
* Map of hourly taxes will be updated automatically.
|
||||
*
|
||||
* @param records to cache
|
||||
*/
|
||||
public void putElectricityTaxes(Collection<DatahubPricelistRecord> records) {
|
||||
putDatahubRecords(electricityTaxRecords, records);
|
||||
updateElectricityTaxes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current "raw"/unprocessed transmission net tariff records in cache.
|
||||
* Map of hourly tariffs will be updated automatically.
|
||||
*
|
||||
* @param records to cache
|
||||
*/
|
||||
public void putTransmissionNetTariffs(Collection<DatahubPricelistRecord> records) {
|
||||
putDatahubRecords(transmissionNetTariffRecords, records);
|
||||
updateTransmissionNetTariffs();
|
||||
}
|
||||
|
||||
private void putDatahubRecords(Collection<DatahubPricelistRecord> destination,
|
||||
Collection<DatahubPricelistRecord> source) {
|
||||
LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE))
|
||||
.minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
|
||||
|
||||
destination.clear();
|
||||
destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map of hourly net tariffs from internal cache.
|
||||
*/
|
||||
public void updateNetTariffs() {
|
||||
netTariffMap = priceListParser.toHourly(netTariffRecords);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map of system tariffs from internal cache.
|
||||
*/
|
||||
public void updateSystemTariffs() {
|
||||
systemTariffMap = priceListParser.toHourly(systemTariffRecords);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map of electricity taxes from internal cache.
|
||||
*/
|
||||
public void updateElectricityTaxes() {
|
||||
electricityTaxMap = priceListParser.toHourly(electricityTaxRecords);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map of hourly transmission net tariffs from internal cache.
|
||||
*/
|
||||
public void updateTransmissionNetTariffs() {
|
||||
transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current spot price.
|
||||
*
|
||||
* @return spot price currently valid
|
||||
*/
|
||||
public @Nullable BigDecimal getSpotPrice() {
|
||||
return getSpotPrice(Instant.now(clock));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spot price valid at provided instant.
|
||||
*
|
||||
* @param time {@link Instant} for which to get the spot price
|
||||
* @return spot price at given time or null if not available
|
||||
*/
|
||||
public @Nullable BigDecimal getSpotPrice(Instant time) {
|
||||
return spotPriceMap.get(getHourStart(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map of all cached spot prices.
|
||||
*
|
||||
* @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getSpotPrices() {
|
||||
return new HashMap<Instant, BigDecimal>(spotPriceMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current net tariff.
|
||||
*
|
||||
* @return net tariff currently valid
|
||||
*/
|
||||
public @Nullable BigDecimal getNetTariff() {
|
||||
return getNetTariff(Instant.now(clock));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get net tariff valid at provided instant.
|
||||
*
|
||||
* @param time {@link Instant} for which to get the net tariff
|
||||
* @return net tariff at given time or null if not available
|
||||
*/
|
||||
public @Nullable BigDecimal getNetTariff(Instant time) {
|
||||
return netTariffMap.get(getHourStart(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map of all cached net tariffs.
|
||||
*
|
||||
* @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getNetTariffs() {
|
||||
return new HashMap<Instant, BigDecimal>(netTariffMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current system tariff.
|
||||
*
|
||||
* @return system tariff currently valid
|
||||
*/
|
||||
public @Nullable BigDecimal getSystemTariff() {
|
||||
return getSystemTariff(Instant.now(clock));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system tariff valid at provided instant.
|
||||
*
|
||||
* @param time {@link Instant} for which to get the system tariff
|
||||
* @return system tariff at given time or null if not available
|
||||
*/
|
||||
public @Nullable BigDecimal getSystemTariff(Instant time) {
|
||||
return systemTariffMap.get(getHourStart(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map of all cached system tariffs.
|
||||
*
|
||||
* @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getSystemTariffs() {
|
||||
return new HashMap<Instant, BigDecimal>(systemTariffMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current electricity tax.
|
||||
*
|
||||
* @return electricity tax currently valid
|
||||
*/
|
||||
public @Nullable BigDecimal getElectricityTax() {
|
||||
return getElectricityTax(Instant.now(clock));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get electricity tax valid at provided instant.
|
||||
*
|
||||
* @param time {@link Instant} for which to get the electricity tax
|
||||
* @return electricity tax at given time or null if not available
|
||||
*/
|
||||
public @Nullable BigDecimal getElectricityTax(Instant time) {
|
||||
return electricityTaxMap.get(getHourStart(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map of all cached electricity taxes.
|
||||
*
|
||||
* @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getElectricityTaxes() {
|
||||
return new HashMap<Instant, BigDecimal>(electricityTaxMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current transmission net tariff.
|
||||
*
|
||||
* @return transmission net tariff currently valid
|
||||
*/
|
||||
public @Nullable BigDecimal getTransmissionNetTariff() {
|
||||
return getTransmissionNetTariff(Instant.now(clock));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmission net tariff valid at provided instant.
|
||||
*
|
||||
* @param time {@link Instant} for which to get the transmission net tariff
|
||||
* @return transmission net tariff at given time or null if not available
|
||||
*/
|
||||
public @Nullable BigDecimal getTransmissionNetTariff(Instant time) {
|
||||
return transmissionNetTariffMap.get(getHourStart(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map of all cached transmission net tariffs.
|
||||
*
|
||||
* @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getTransmissionNetTariffs() {
|
||||
return new HashMap<Instant, BigDecimal>(transmissionNetTariffMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of future spot prices including current hour.
|
||||
*
|
||||
* @return number of future spot prices
|
||||
*/
|
||||
public long getNumberOfFutureSpotPrices() {
|
||||
Instant currentHourStart = getCurrentHourStart();
|
||||
|
||||
return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
|
||||
*
|
||||
* @return true if historic spot prices are cached
|
||||
*/
|
||||
public boolean areHistoricSpotPricesCached() {
|
||||
return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all current spot prices are cached taking into consideration that next day's spot prices
|
||||
* should be available at 13:00 CET.
|
||||
*
|
||||
* @return true if spot prices are fully cached
|
||||
*/
|
||||
public boolean areSpotPricesFullyCached() {
|
||||
Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant();
|
||||
LocalTime now = LocalTime.now(clock);
|
||||
if (now.isAfter(DAILY_REFRESH_TIME_CET)) {
|
||||
end = end.plus(24, ChronoUnit.HOURS);
|
||||
}
|
||||
|
||||
return arePricesCached(spotPriceMap, end);
|
||||
}
|
||||
|
||||
private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
|
||||
for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
|
||||
ChronoUnit.HOURS)) {
|
||||
if (priceMap.get(hourStart) == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have "raw" net tariff records cached which are valid tomorrow.
|
||||
*
|
||||
* @return true if net tariff records for tomorrow are cached
|
||||
*/
|
||||
public boolean areNetTariffsValidTomorrow() {
|
||||
return isValidNextDay(netTariffRecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have "raw" system tariff records cached which are valid tomorrow.
|
||||
*
|
||||
* @return true if system tariff records for tomorrow are cached
|
||||
*/
|
||||
public boolean areSystemTariffsValidTomorrow() {
|
||||
return isValidNextDay(systemTariffRecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have "raw" electricity tax records cached which are valid tomorrow.
|
||||
*
|
||||
* @return true if electricity tax records for tomorrow are cached
|
||||
*/
|
||||
public boolean areElectricityTaxesValidTomorrow() {
|
||||
return isValidNextDay(electricityTaxRecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have "raw" transmission net tariff records cached which are valid tomorrow.
|
||||
*
|
||||
* @return true if transmission net tariff records for tomorrow are cached
|
||||
*/
|
||||
public boolean areTransmissionNetTariffsValidTomorrow() {
|
||||
return isValidNextDay(transmissionNetTariffRecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove historic prices.
|
||||
*/
|
||||
public void cleanup() {
|
||||
Instant firstHourStart = getFirstHourStart();
|
||||
|
||||
spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
|
||||
netTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
|
||||
systemTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
|
||||
electricityTaxMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
|
||||
transmissionNetTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
|
||||
}
|
||||
|
||||
private boolean isValidNextDay(Collection<DatahubPricelistRecord> records) {
|
||||
LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
|
||||
.truncatedTo(ChronoUnit.HOURS);
|
||||
LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
|
||||
}
|
||||
|
||||
private Instant getCurrentHourStart() {
|
||||
return getHourStart(Instant.now(clock));
|
||||
}
|
||||
|
||||
private Instant getFirstHourStart() {
|
||||
return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
|
||||
}
|
||||
|
||||
private Instant getHourStart(Instant instant) {
|
||||
return instant.truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Currency;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link EnergiDataServiceBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EnergiDataServiceBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "energidataservice";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_SERVICE = new ThingTypeUID(BINDING_ID, "service");
|
||||
|
||||
// List of all Channel Group ids
|
||||
public static final String CHANNEL_GROUP_ELECTRICITY = "electricity";
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_SPOT_PRICE = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
|
||||
+ "spot-price";
|
||||
public static final String CHANNEL_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
|
||||
+ "net-tariff";
|
||||
public static final String CHANNEL_SYSTEM_TARIFF = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
|
||||
+ "system-tariff";
|
||||
public static final String CHANNEL_ELECTRICITY_TAX = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
|
||||
+ "electricity-tax";
|
||||
public static final String CHANNEL_TRANSMISSION_NET_TARIFF = CHANNEL_GROUP_ELECTRICITY
|
||||
+ ChannelUID.CHANNEL_GROUP_SEPARATOR + "transmission-net-tariff";
|
||||
public static final String CHANNEL_HOURLY_PRICES = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR
|
||||
+ "hourly-prices";
|
||||
|
||||
public static final Set<String> ELECTRICITY_CHANNELS = Set.of(CHANNEL_SPOT_PRICE, CHANNEL_NET_TARIFF,
|
||||
CHANNEL_SYSTEM_TARIFF, CHANNEL_ELECTRICITY_TAX, CHANNEL_TRANSMISSION_NET_TARIFF, CHANNEL_HOURLY_PRICES);
|
||||
|
||||
// List of all properties
|
||||
public static final String PROPERTY_REMAINING_CALLS = "remainingCalls";
|
||||
public static final String PROPERTY_TOTAL_CALLS = "totalCalls";
|
||||
public static final String PROPERTY_LAST_CALL = "lastCall";
|
||||
public static final String PROPERTY_NEXT_CALL = "nextCall";
|
||||
|
||||
// List of supported currencies
|
||||
public static final Currency CURRENCY_DKK = Currency.getInstance("DKK");
|
||||
public static final Currency CURRENCY_EUR = Currency.getInstance("EUR");
|
||||
|
||||
public static final Set<Currency> SUPPORTED_CURRENCIES = Set.of(CURRENCY_DKK, CURRENCY_EUR);
|
||||
|
||||
// Time-zone of Datahub
|
||||
public static final ZoneId DATAHUB_TIMEZONE = ZoneId.of("CET");
|
||||
public static final ZoneId NORD_POOL_TIMEZONE = ZoneId.of("CET");
|
||||
|
||||
// Other
|
||||
public static final LocalTime DAILY_REFRESH_TIME_CET = LocalTime.of(13, 0);
|
||||
public static final LocalDate ENERGINET_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
|
||||
public static final String PROPERTY_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.quantity.Energy;
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides calculations based on price maps.
|
||||
* This is the current stage of evolution.
|
||||
* Ideally this binding would simply provide data in a well-defined format for
|
||||
* openHAB core. Operations on this data could then be implemented in core.
|
||||
* This way there would be a unified interface from rules, and the calculations
|
||||
* could be reused between different data providers (bindings).
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PriceCalculator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PriceCalculator.class);
|
||||
|
||||
private final Map<Instant, BigDecimal> priceMap;
|
||||
|
||||
public PriceCalculator(Map<Instant, BigDecimal> priceMap) {
|
||||
this.priceMap = priceMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cheapest period from list of durations with specified amount of energy
|
||||
* used per phase.
|
||||
*
|
||||
* @param earliestStart Earliest allowed start time.
|
||||
* @param latestEnd Latest allowed end time.
|
||||
* @param totalDuration Total duration to fit.
|
||||
* @param durationPhases List of {@link Duration}'s representing different phases of using power.
|
||||
* @param energyUsedPerPhase Amount of energy used per phase.
|
||||
*
|
||||
* @return Map containing resulting values
|
||||
*/
|
||||
public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration,
|
||||
Collection<Duration> durationPhases, QuantityType<Energy> energyUsedPerPhase) throws MissingPriceException {
|
||||
QuantityType<Energy> energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR);
|
||||
if (energyInWattHour == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid unit " + energyUsedPerPhase.getUnit() + ", expected energy unit");
|
||||
}
|
||||
// watts = (kWh × 1,000) ÷ hrs
|
||||
int numerator = energyInWattHour.intValue() * 3600;
|
||||
List<QuantityType<Power>> consumptionPhases = new ArrayList<>();
|
||||
Duration remainingDuration = totalDuration;
|
||||
for (Duration phase : durationPhases) {
|
||||
consumptionPhases.add(QuantityType.valueOf(numerator / phase.getSeconds(), Units.WATT));
|
||||
remainingDuration = remainingDuration.minus(phase);
|
||||
}
|
||||
if (remainingDuration.isNegative()) {
|
||||
throw new IllegalArgumentException("totalDuration must be equal to or greater than sum of phases");
|
||||
}
|
||||
if (!remainingDuration.isZero()) {
|
||||
List<Duration> durationsWithTermination = new ArrayList<>(durationPhases);
|
||||
durationsWithTermination.add(remainingDuration);
|
||||
consumptionPhases.add(QuantityType.valueOf(0, Units.WATT));
|
||||
return calculateCheapestPeriod(earliestStart, latestEnd, durationsWithTermination, consumptionPhases);
|
||||
}
|
||||
return calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, consumptionPhases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cheapest period from duration with linear power usage.
|
||||
*
|
||||
* @param earliestStart Earliest allowed start time.
|
||||
* @param latestEnd Latest allowed end time.
|
||||
* @param duration Duration to fit.
|
||||
* @param power Power consumption for the duration of time.
|
||||
*
|
||||
* @return Map containing resulting values
|
||||
*/
|
||||
public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration,
|
||||
QuantityType<Power> power) throws MissingPriceException {
|
||||
return calculateCheapestPeriod(earliestStart, latestEnd, List.of(duration), List.of(power));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cheapest period from list of durations with corresponding list of consumption
|
||||
* per duration.
|
||||
*
|
||||
* @param earliestStart Earliest allowed start time.
|
||||
* @param latestEnd Latest allowed end time.
|
||||
* @param durationPhases List of {@link Duration}'s representing different phases of using power.
|
||||
* @param consumptionPhases Corresponding List of power consumption for the duration of time.
|
||||
*
|
||||
* @return Map containing resulting values
|
||||
*/
|
||||
public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd,
|
||||
Collection<Duration> durationPhases, Collection<QuantityType<Power>> consumptionPhases)
|
||||
throws MissingPriceException {
|
||||
if (durationPhases.size() != consumptionPhases.size()) {
|
||||
throw new IllegalArgumentException("Number of phases do not match");
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Duration totalDuration = durationPhases.stream().reduce(Duration.ZERO, Duration::plus);
|
||||
Instant calculationStart = earliestStart;
|
||||
Instant calculationEnd = earliestStart.plus(totalDuration);
|
||||
BigDecimal lowestPrice = BigDecimal.valueOf(Double.MAX_VALUE);
|
||||
BigDecimal highestPrice = BigDecimal.ZERO;
|
||||
Instant cheapestStart = Instant.MIN;
|
||||
Instant mostExpensiveStart = Instant.MIN;
|
||||
|
||||
while (calculationEnd.compareTo(latestEnd) <= 0) {
|
||||
BigDecimal currentPrice = BigDecimal.ZERO;
|
||||
Duration minDurationUntilNextHour = Duration.ofHours(1);
|
||||
Instant atomStart = calculationStart;
|
||||
|
||||
Iterator<Duration> durationIterator = durationPhases.iterator();
|
||||
Iterator<QuantityType<Power>> consumptionIterator = consumptionPhases.iterator();
|
||||
while (durationIterator.hasNext()) {
|
||||
Duration atomDuration = durationIterator.next();
|
||||
QuantityType<Power> atomConsumption = consumptionIterator.next();
|
||||
|
||||
Instant atomEnd = atomStart.plus(atomDuration);
|
||||
Instant hourStart = atomStart.truncatedTo(ChronoUnit.HOURS);
|
||||
Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
|
||||
|
||||
// Get next intersection with hourly rate change.
|
||||
Duration durationUntilNextHour = Duration.between(atomStart, hourEnd);
|
||||
if (durationUntilNextHour.compareTo(minDurationUntilNextHour) < 0) {
|
||||
minDurationUntilNextHour = durationUntilNextHour;
|
||||
}
|
||||
|
||||
BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, atomConsumption);
|
||||
currentPrice = currentPrice.add(atomPrice);
|
||||
atomStart = atomEnd;
|
||||
}
|
||||
|
||||
if (currentPrice.compareTo(lowestPrice) < 0) {
|
||||
lowestPrice = currentPrice;
|
||||
cheapestStart = calculationStart;
|
||||
}
|
||||
if (currentPrice.compareTo(highestPrice) > 0) {
|
||||
highestPrice = currentPrice;
|
||||
mostExpensiveStart = calculationStart;
|
||||
}
|
||||
|
||||
// Now fast forward to next hourly rate intersection.
|
||||
calculationStart = calculationStart.plus(minDurationUntilNextHour);
|
||||
calculationEnd = calculationStart.plus(totalDuration);
|
||||
}
|
||||
|
||||
if (!cheapestStart.equals(Instant.MIN)) {
|
||||
result.put("CheapestStart", cheapestStart);
|
||||
result.put("LowestPrice", lowestPrice);
|
||||
result.put("MostExpensiveStart", mostExpensiveStart);
|
||||
result.put("HighestPrice", highestPrice);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total price from 'start' to 'end' given linear power consumption.
|
||||
*
|
||||
* @param start Start time
|
||||
* @param end End time
|
||||
* @param power The current power consumption.
|
||||
*/
|
||||
public BigDecimal calculatePrice(Instant start, Instant end, QuantityType<Power> power)
|
||||
throws MissingPriceException {
|
||||
QuantityType<Power> quantityInWatt = power.toUnit(Units.WATT);
|
||||
if (quantityInWatt == null) {
|
||||
throw new IllegalArgumentException("Invalid unit " + power.getUnit() + ", expected power unit");
|
||||
}
|
||||
BigDecimal watt = new BigDecimal(quantityInWatt.intValue());
|
||||
if (watt.equals(BigDecimal.ZERO)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
Instant current = start;
|
||||
BigDecimal result = BigDecimal.ZERO;
|
||||
while (current.isBefore(end)) {
|
||||
Instant hourStart = current.truncatedTo(ChronoUnit.HOURS);
|
||||
Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
|
||||
|
||||
BigDecimal currentPrice = priceMap.get(hourStart);
|
||||
if (currentPrice == null) {
|
||||
throw new MissingPriceException("Price missing at " + hourStart.toString());
|
||||
}
|
||||
|
||||
Instant currentStart = hourStart;
|
||||
if (start.isAfter(hourStart)) {
|
||||
currentStart = start;
|
||||
}
|
||||
Instant currentEnd = hourEnd;
|
||||
if (end.isBefore(hourEnd)) {
|
||||
currentEnd = end;
|
||||
}
|
||||
|
||||
// E(kWh) = P(W) × t(hr) / 1000
|
||||
Duration duration = Duration.between(currentStart, currentEnd);
|
||||
BigDecimal contribution = currentPrice.multiply(watt).multiply(
|
||||
new BigDecimal(duration.getSeconds()).divide(new BigDecimal(3600000), 9, RoundingMode.HALF_UP));
|
||||
result = result.add(contribution);
|
||||
logger.trace("Period {}-{}: {} @ {}", currentStart, currentEnd, contribution, currentPrice);
|
||||
|
||||
current = hourEnd;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
|
||||
|
||||
/**
|
||||
* Parses results from {@link DatahubPricelistRecords} into map of hourly tariffs.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PriceListParser {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public PriceListParser() {
|
||||
this(Clock.system(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
}
|
||||
|
||||
public PriceListParser(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records) {
|
||||
Map<Instant, BigDecimal> totalMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
|
||||
records.stream().map(record -> record.chargeTypeCode()).distinct().forEach(chargeTypeCode -> {
|
||||
Map<Instant, BigDecimal> currentMap = toHourly(records, chargeTypeCode);
|
||||
for (Entry<Instant, BigDecimal> current : currentMap.entrySet()) {
|
||||
BigDecimal total = totalMap.get(current.getKey());
|
||||
if (total == null) {
|
||||
total = BigDecimal.ZERO;
|
||||
}
|
||||
totalMap.put(current.getKey(), total.add(current.getValue()));
|
||||
}
|
||||
});
|
||||
|
||||
return totalMap;
|
||||
}
|
||||
|
||||
public Map<Instant, BigDecimal> toHourly(Collection<DatahubPricelistRecord> records, String chargeTypeCode) {
|
||||
Map<Instant, BigDecimal> tariffMap = new ConcurrentHashMap<>(CacheManager.TARIFF_MAX_CACHE_SIZE);
|
||||
|
||||
Instant firstHourStart = Instant.now(clock).minus(CacheManager.NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS)
|
||||
.truncatedTo(ChronoUnit.HOURS);
|
||||
Instant lastHourStart = Instant.now(clock).truncatedTo(ChronoUnit.HOURS).plus(2, ChronoUnit.DAYS)
|
||||
.truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
LocalDateTime previousValidFrom = LocalDateTime.MAX;
|
||||
LocalDateTime previousValidTo = LocalDateTime.MIN;
|
||||
Map<LocalTime, BigDecimal> tariffs = Map.of();
|
||||
for (Instant hourStart = firstHourStart; hourStart
|
||||
.isBefore(lastHourStart); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) {
|
||||
LocalDateTime localDateTime = hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
|
||||
.toLocalDateTime();
|
||||
if (localDateTime.compareTo(previousValidFrom) < 0 || localDateTime.compareTo(previousValidTo) >= 0) {
|
||||
DatahubPricelistRecord priceList = getTariffs(records, localDateTime, chargeTypeCode);
|
||||
if (priceList != null) {
|
||||
tariffs = priceList.getTariffMap();
|
||||
previousValidFrom = priceList.validFrom();
|
||||
previousValidTo = priceList.validTo();
|
||||
} else {
|
||||
tariffs = Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
LocalTime localTime = LocalTime
|
||||
.of(hourStart.atZone(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE).getHour(), 0);
|
||||
BigDecimal tariff = tariffs.get(localTime);
|
||||
if (tariff != null) {
|
||||
tariffMap.put(hourStart, tariff);
|
||||
}
|
||||
}
|
||||
|
||||
return tariffMap;
|
||||
}
|
||||
|
||||
private @Nullable DatahubPricelistRecord getTariffs(Collection<DatahubPricelistRecord> records,
|
||||
LocalDateTime localDateTime, String chargeTypeCode) {
|
||||
return records.stream()
|
||||
.filter(record -> localDateTime.compareTo(record.validFrom()) >= 0
|
||||
&& localDateTime.compareTo(record.validTo()) < 0
|
||||
&& record.chargeTypeCode().equals(chargeTypeCode))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.action;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.quantity.Energy;
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.energidataservice.internal.PriceCalculator;
|
||||
import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
|
||||
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.ActionOutput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@ThingActionsScope(name = "energidataservice")
|
||||
@NonNullByDefault
|
||||
public class EnergiDataServiceActions implements ThingActions {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
|
||||
|
||||
private @Nullable EnergiDataServiceHandler handler;
|
||||
|
||||
private enum PriceElement {
|
||||
SPOT_PRICE("spotprice"),
|
||||
NET_TARIFF("nettariff"),
|
||||
SYSTEM_TARIFF("systemtariff"),
|
||||
ELECTRICITY_TAX("electricitytax"),
|
||||
TRANSMISSION_NET_TARIFF("transmissionnettariff");
|
||||
|
||||
private static final Map<String, PriceElement> NAME_MAP = Stream.of(values())
|
||||
.collect(Collectors.toMap(PriceElement::toString, Function.identity()));
|
||||
|
||||
private String name;
|
||||
|
||||
private PriceElement(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public static PriceElement fromString(final String name) {
|
||||
PriceElement myEnum = NAME_MAP.get(name.toLowerCase());
|
||||
if (null == myEnum) {
|
||||
throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
|
||||
name, Arrays.asList(values())));
|
||||
}
|
||||
return myEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
|
||||
public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices() {
|
||||
return getPrices(Arrays.stream(PriceElement.values()).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
|
||||
public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices(
|
||||
@ActionInput(name = "priceElements", label = "@text/action.get-prices.priceElements.label", description = "@text/action.get-prices.priceElements.description") @Nullable String priceElements) {
|
||||
if (priceElements == null) {
|
||||
logger.warn("Argument 'priceElements' is null");
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
Set<PriceElement> priceElementsSet;
|
||||
try {
|
||||
priceElementsSet = new HashSet<PriceElement>(
|
||||
Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
return getPrices(priceElementsSet);
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.calculate-price.label", description = "@text/action.calculate-price.description")
|
||||
public @ActionOutput(name = "price", type = "java.math.BigDecimal") BigDecimal calculatePrice(
|
||||
@ActionInput(name = "start", type = "java.time.Instant") Instant start,
|
||||
@ActionInput(name = "end", type = "java.time.Instant") Instant end,
|
||||
@ActionInput(name = "power", type = "QuantityType<Power>") QuantityType<Power> power) {
|
||||
PriceCalculator priceCalculator = new PriceCalculator(getPrices());
|
||||
|
||||
try {
|
||||
return priceCalculator.calculatePrice(start, end, power);
|
||||
} catch (MissingPriceException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
|
||||
public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
|
||||
@ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
|
||||
@ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
|
||||
@ActionInput(name = "duration", type = "java.time.Duration") Duration duration) {
|
||||
PriceCalculator priceCalculator = new PriceCalculator(getPrices());
|
||||
|
||||
try {
|
||||
Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
|
||||
duration, QuantityType.valueOf(1000, Units.WATT));
|
||||
|
||||
// Create new result with stripped price information.
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Object value = intermediateResult.get("CheapestStart");
|
||||
if (value != null) {
|
||||
result.put("CheapestStart", value);
|
||||
}
|
||||
value = intermediateResult.get("MostExpensiveStart");
|
||||
if (value != null) {
|
||||
result.put("MostExpensiveStart", value);
|
||||
}
|
||||
return result;
|
||||
} catch (MissingPriceException | IllegalArgumentException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
|
||||
public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
|
||||
@ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
|
||||
@ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
|
||||
@ActionInput(name = "duration", type = "java.time.Duration") Duration duration,
|
||||
@ActionInput(name = "power", type = "QuantityType<Power>") QuantityType<Power> power) {
|
||||
PriceCalculator priceCalculator = new PriceCalculator(getPrices());
|
||||
|
||||
try {
|
||||
return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
|
||||
} catch (MissingPriceException | IllegalArgumentException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
|
||||
public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
|
||||
@ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
|
||||
@ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
|
||||
@ActionInput(name = "totalDuration", type = "java.time.Duration") Duration totalDuration,
|
||||
@ActionInput(name = "durationPhases", type = "java.util.List<java.time.Duration>") List<Duration> durationPhases,
|
||||
@ActionInput(name = "energyUsedPerPhase", type = "QuantityType<Energy>") QuantityType<Energy> energyUsedPerPhase) {
|
||||
PriceCalculator priceCalculator = new PriceCalculator(getPrices());
|
||||
|
||||
try {
|
||||
return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
|
||||
energyUsedPerPhase);
|
||||
} catch (MissingPriceException | IllegalArgumentException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
|
||||
public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
|
||||
@ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
|
||||
@ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
|
||||
@ActionInput(name = "durationPhases", type = "java.util.List<java.time.Duration>") List<Duration> durationPhases,
|
||||
@ActionInput(name = "powerPhases", type = "java.util.List<QuantityType<Power>>") List<QuantityType<Power>> powerPhases) {
|
||||
if (durationPhases.size() != powerPhases.size()) {
|
||||
logger.warn("Number of duration phases ({}) is different from number of consumption phases ({})",
|
||||
durationPhases.size(), powerPhases.size());
|
||||
return Map.of();
|
||||
}
|
||||
PriceCalculator priceCalculator = new PriceCalculator(getPrices());
|
||||
|
||||
try {
|
||||
return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
|
||||
} catch (MissingPriceException | IllegalArgumentException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Instant, BigDecimal> getPrices(Set<PriceElement> priceElements) {
|
||||
EnergiDataServiceHandler handler = this.handler;
|
||||
if (handler == null) {
|
||||
logger.warn("EnergiDataServiceActions ThingHandler is null.");
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
Map<Instant, BigDecimal> prices;
|
||||
boolean spotPricesRequired;
|
||||
if (priceElements.contains(PriceElement.SPOT_PRICE)) {
|
||||
if (priceElements.size() > 1 && !handler.getCurrency().equals(CURRENCY_DKK)) {
|
||||
logger.warn("Cannot calculate sum when spot price currency is {}", handler.getCurrency());
|
||||
return Map.of();
|
||||
}
|
||||
prices = handler.getSpotPrices();
|
||||
spotPricesRequired = true;
|
||||
} else {
|
||||
spotPricesRequired = false;
|
||||
prices = new HashMap<>();
|
||||
}
|
||||
|
||||
if (priceElements.contains(PriceElement.NET_TARIFF)) {
|
||||
Map<Instant, BigDecimal> netTariffMap = handler.getNetTariffs();
|
||||
mergeMaps(prices, netTariffMap, !spotPricesRequired);
|
||||
}
|
||||
|
||||
if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
|
||||
Map<Instant, BigDecimal> systemTariffMap = handler.getSystemTariffs();
|
||||
mergeMaps(prices, systemTariffMap, !spotPricesRequired);
|
||||
}
|
||||
|
||||
if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
|
||||
Map<Instant, BigDecimal> electricityTaxMap = handler.getElectricityTaxes();
|
||||
mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
|
||||
}
|
||||
|
||||
if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) {
|
||||
Map<Instant, BigDecimal> transmissionNetTariffMap = handler.getTransmissionNetTariffs();
|
||||
mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired);
|
||||
}
|
||||
|
||||
return prices;
|
||||
}
|
||||
|
||||
private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
|
||||
boolean createNew) {
|
||||
for (Entry<Instant, BigDecimal> source : sourceMap.entrySet()) {
|
||||
Instant key = source.getKey();
|
||||
BigDecimal sourceValue = source.getValue();
|
||||
BigDecimal destinationValue = destinationMap.get(key);
|
||||
if (destinationValue != null) {
|
||||
destinationMap.put(key, sourceValue.add(destinationValue));
|
||||
} else if (createNew) {
|
||||
destinationMap.put(key, sourceValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static get prices method for DSL rule compatibility.
|
||||
*
|
||||
* @param actions
|
||||
* @param priceElements Comma-separated list of price elements to include in prices.
|
||||
* @return Map of prices
|
||||
*/
|
||||
public static Map<Instant, BigDecimal> getPrices(@Nullable ThingActions actions, @Nullable String priceElements) {
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
if (priceElements != null && !priceElements.isBlank()) {
|
||||
return ((EnergiDataServiceActions) actions).getPrices(priceElements);
|
||||
} else {
|
||||
return ((EnergiDataServiceActions) actions).getPrices();
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static get prices method for DSL rule compatibility.
|
||||
*
|
||||
* @param actions
|
||||
* @param start Start time
|
||||
* @param end End time
|
||||
* @param power Constant power consumption
|
||||
* @return Map of prices
|
||||
*/
|
||||
public static BigDecimal calculatePrice(@Nullable ThingActions actions, @Nullable Instant start,
|
||||
@Nullable Instant end, @Nullable QuantityType<Power> power) {
|
||||
if (start == null || end == null || power == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
return ((EnergiDataServiceActions) actions).calculatePrice(start, end, power);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
|
||||
@Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration) {
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
if (earliestStart == null || latestEnd == null || duration == null) {
|
||||
return Map.of();
|
||||
}
|
||||
return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
|
||||
@Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration,
|
||||
@Nullable QuantityType<Power> power) {
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
if (earliestStart == null || latestEnd == null || duration == null || power == null) {
|
||||
return Map.of();
|
||||
}
|
||||
return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, duration,
|
||||
power);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
|
||||
@Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration,
|
||||
@Nullable List<Duration> durationPhases, @Nullable QuantityType<Energy> energyUsedPerPhase) {
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
if (earliestStart == null || latestEnd == null || totalDuration == null || durationPhases == null
|
||||
|| energyUsedPerPhase == null) {
|
||||
return Map.of();
|
||||
}
|
||||
return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd, totalDuration,
|
||||
durationPhases, energyUsedPerPhase);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
|
||||
@Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List<Duration> durationPhases,
|
||||
@Nullable List<QuantityType<Power>> powerPhases) {
|
||||
if (actions instanceof EnergiDataServiceActions) {
|
||||
if (earliestStart == null || latestEnd == null || durationPhases == null || powerPhases == null) {
|
||||
return Map.of();
|
||||
}
|
||||
return ((EnergiDataServiceActions) actions).calculateCheapestPeriod(earliestStart, latestEnd,
|
||||
durationPhases, powerPhases);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof EnergiDataServiceHandler) {
|
||||
this.handler = (EnergiDataServiceHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Filter for the {@link DatahubPricelist} dataset.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DatahubTariffFilter {
|
||||
|
||||
private final Set<ChargeTypeCode> chargeTypeCodes;
|
||||
private final Set<String> notes;
|
||||
private final DateQueryParameter dateQueryParameter;
|
||||
|
||||
public DatahubTariffFilter(DatahubTariffFilter filter, DateQueryParameter dateQueryParameter) {
|
||||
this(filter.chargeTypeCodes, filter.notes, dateQueryParameter);
|
||||
}
|
||||
|
||||
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes) {
|
||||
this(chargeTypeCodes, notes, DateQueryParameter.EMPTY);
|
||||
}
|
||||
|
||||
public DatahubTariffFilter(Set<ChargeTypeCode> chargeTypeCodes, Set<String> notes,
|
||||
DateQueryParameter dateQueryParameter) {
|
||||
this.chargeTypeCodes = chargeTypeCodes;
|
||||
this.notes = notes;
|
||||
this.dateQueryParameter = dateQueryParameter;
|
||||
}
|
||||
|
||||
public Collection<String> getChargeTypeCodesAsStrings() {
|
||||
return chargeTypeCodes.stream().map(c -> c.toString()).toList();
|
||||
}
|
||||
|
||||
public Collection<String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public DateQueryParameter getDateQueryParameter() {
|
||||
return dateQueryParameter;
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Factory for creating a {@link DatahubTariffFilter} for a specific Grid Company GLN.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DatahubTariffFilterFactory {
|
||||
|
||||
private static final String GLN_CERIUS = "5790000705184";
|
||||
private static final String GLN_DINEL = "5790000610099";
|
||||
private static final String GLN_ELEKTRUS = "5790000836239";
|
||||
private static final String GLN_ELINORD = "5790001095277";
|
||||
private static final String GLN_ELNET_MIDT = "5790001100520";
|
||||
private static final String GLN_ELNET_KONGERSLEV = "5790002502699";
|
||||
private static final String GLN_FLOW_ELNET = "5790000392551";
|
||||
private static final String GLN_HAMMEL_ELFORSYNING_NET = "5790001090166";
|
||||
private static final String GLN_HURUP_ELVAERK_NET = "5790000610839";
|
||||
private static final String GLN_IKAST_E1_NET = "5790000682102";
|
||||
private static final String GLN_KONSTANT = "5790000704842";
|
||||
private static final String GLN_L_NET = "5790001090111";
|
||||
private static final String GLN_MIDTFYNS_ELFORSYNING = "5790001089023";
|
||||
private static final String GLN_N1 = "5790001089030";
|
||||
private static final String GLN_NETSELSKABET_ELVAERK = "5790000681075";
|
||||
private static final String GLN_NKE_ELNET = "5790001088231";
|
||||
private static final String GLN_NORD_ENERGI_NET = "5790000610877";
|
||||
private static final String GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET = "5790000395620";
|
||||
private static final String GLN_RADIUS = "5790000705689";
|
||||
private static final String GLN_RAH_NET = "5790000681327";
|
||||
private static final String GLN_RAVDEX = "5790000836727";
|
||||
private static final String GLN_SUNDS_NET = "5790001095444";
|
||||
private static final String GLN_TARM_ELVAERK_NET = "5790000706419";
|
||||
private static final String GLN_TREFOR_EL_NET = "5790000392261";
|
||||
private static final String GLN_TREFOR_EL_NET_OEST = "5790000706686";
|
||||
private static final String GLN_VEKSEL = "5790001088217";
|
||||
private static final String GLN_VORES_ELNET = "5790000610976";
|
||||
private static final String GLN_ZEANET = "5790001089375";
|
||||
|
||||
private static final String NOTE_NET_TARIFF = "Nettarif";
|
||||
private static final String NOTE_NET_TARIFF_C = NOTE_NET_TARIFF + " C";
|
||||
private static final String NOTE_NET_TARIFF_C_HOUR = NOTE_NET_TARIFF_C + " time";
|
||||
private static final String NOTE_NET_TARIFF_C_FLEX = NOTE_NET_TARIFF_C + " Flex";
|
||||
private static final String NOTE_NET_TARIFF_C_FLEX_HOUR = NOTE_NET_TARIFF_C_FLEX + " - time";
|
||||
private static final String NOTE_SYSTEM_TARIFF = "Systemtarif";
|
||||
private static final String NOTE_ELECTRICITY_TAX = "Elafgift";
|
||||
private static final String NOTE_TRANSMISSION_NET_TARIFF = "Transmissions nettarif";
|
||||
|
||||
public static final LocalDate N1_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
|
||||
public static final LocalDate RADIUS_CUTOFF_DATE = LocalDate.of(2023, 1, 1);
|
||||
public static final LocalDate KONSTANT_CUTOFF_DATE = LocalDate.of(2023, 2, 1);
|
||||
|
||||
public static DatahubTariffFilter getNetTariffByGLN(String globalLocationNumber) {
|
||||
switch (globalLocationNumber) {
|
||||
case GLN_CERIUS:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("30TR_C_ET")), Set.of(NOTE_NET_TARIFF_C_HOUR));
|
||||
case GLN_DINEL:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TCL>100_02")), Set.of(NOTE_NET_TARIFF_C_HOUR));
|
||||
case GLN_ELEKTRUS:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("6000091")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_ELINORD:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("43300")),
|
||||
Set.of("Transportbetaling, eget net C"));
|
||||
case GLN_ELNET_MIDT:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("T3002")), Set.of(NOTE_NET_TARIFF_C),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_ELNET_KONGERSLEV:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("K_22100")), Set.of(NOTE_NET_TARIFF_C));
|
||||
case GLN_FLOW_ELNET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("FE2 NT-01")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_HAMMEL_ELFORSYNING_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("50001")), Set.of("Overliggende net"),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_HURUP_ELVAERK_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("HEV-NT-01")), Set.of(NOTE_NET_TARIFF));
|
||||
case GLN_IKAST_E1_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("IEV-NT-01"), ChargeTypeCode.of("IEV-NT-11")),
|
||||
Set.of(NOTE_NET_TARIFF_C_HOUR, "Transport - Overordnet net"));
|
||||
case GLN_KONSTANT:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("151-NT01T"), ChargeTypeCode.of("151-NRA04T")),
|
||||
Set.of(), DateQueryParameter.of(KONSTANT_CUTOFF_DATE));
|
||||
case GLN_L_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("4010")), Set.of(NOTE_NET_TARIFF_C_HOUR));
|
||||
case GLN_MIDTFYNS_ELFORSYNING:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TNT15000")), Set.of(NOTE_NET_TARIFF_C_FLEX),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_N1:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("CD"), ChargeTypeCode.of("CD R")), Set.of(),
|
||||
DateQueryParameter.of(N1_CUTOFF_DATE));
|
||||
case GLN_NETSELSKABET_ELVAERK:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("0NCFF")), Set.of(NOTE_NET_TARIFF_C + " Flex"),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_NKE_ELNET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("94TR_C_ET")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_NORD_ENERGI_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TA031U200")), Set.of(NOTE_NET_TARIFF_C));
|
||||
case GLN_NORDVESTJYSK_ELFORSYNING_NOE_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("Net C")), Set.of(NOTE_NET_TARIFF_C));
|
||||
case GLN_RADIUS:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("DT_C_01")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(RADIUS_CUTOFF_DATE));
|
||||
case GLN_RAH_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("RAH-C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_RAVDEX:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_SUNDS_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("SEF-NT-05")),
|
||||
Set.of(NOTE_NET_TARIFF_C_FLEX_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_TARM_ELVAERK_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TEV-NT-01")), Set.of(NOTE_NET_TARIFF_C));
|
||||
case GLN_TREFOR_EL_NET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("C")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_TREFOR_EL_NET_OEST:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("46")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_VEKSEL:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("NT-10")),
|
||||
Set.of(NOTE_NET_TARIFF_C_HOUR + " NT-10"));
|
||||
case GLN_VORES_ELNET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("TNT1009")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
case GLN_ZEANET:
|
||||
return new DatahubTariffFilter(Set.of(ChargeTypeCode.of("43110")), Set.of(NOTE_NET_TARIFF_C_HOUR),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_DAY, Duration.ofDays(-1)));
|
||||
default:
|
||||
return new DatahubTariffFilter(Set.of(), Set.of(NOTE_NET_TARIFF_C),
|
||||
DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR));
|
||||
}
|
||||
}
|
||||
|
||||
public static DatahubTariffFilter getSystemTariff() {
|
||||
return new DatahubTariffFilter(Set.of(), Set.of(NOTE_SYSTEM_TARIFF),
|
||||
DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
|
||||
}
|
||||
|
||||
public static DatahubTariffFilter getElectricityTax() {
|
||||
return new DatahubTariffFilter(Set.of(), Set.of(NOTE_ELECTRICITY_TAX),
|
||||
DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
|
||||
}
|
||||
|
||||
public static DatahubTariffFilter getTransmissionNetTariff() {
|
||||
return new DatahubTariffFilter(Set.of(), Set.of(NOTE_TRANSMISSION_NET_TARIFF),
|
||||
DateQueryParameter.of(ENERGINET_CUTOFF_DATE));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Record as part of {@link DatahubPricelistRecords} from Energi Data Service.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record DatahubPricelistRecord(@SerializedName("ValidFrom") LocalDateTime validFrom,
|
||||
@SerializedName("ValidTo") LocalDateTime validTo, @SerializedName("ChargeTypeCode") String chargeTypeCode,
|
||||
@SerializedName("Price1") BigDecimal price1, @SerializedName("Price2") BigDecimal price2,
|
||||
@SerializedName("Price3") BigDecimal price3, @SerializedName("Price4") BigDecimal price4,
|
||||
@SerializedName("Price5") BigDecimal price5, @SerializedName("Price6") BigDecimal price6,
|
||||
@SerializedName("Price7") BigDecimal price7, @SerializedName("Price8") BigDecimal price8,
|
||||
@SerializedName("Price9") BigDecimal price9, @SerializedName("Price10") BigDecimal price10,
|
||||
@SerializedName("Price11") BigDecimal price11, @SerializedName("Price12") BigDecimal price12,
|
||||
@SerializedName("Price13") BigDecimal price13, @SerializedName("Price14") BigDecimal price14,
|
||||
@SerializedName("Price15") BigDecimal price15, @SerializedName("Price16") BigDecimal price16,
|
||||
@SerializedName("Price17") BigDecimal price17, @SerializedName("Price18") BigDecimal price18,
|
||||
@SerializedName("Price19") BigDecimal price19, @SerializedName("Price20") BigDecimal price20,
|
||||
@SerializedName("Price21") BigDecimal price21, @SerializedName("Price22") BigDecimal price22,
|
||||
@SerializedName("Price23") BigDecimal price23, @SerializedName("Price24") BigDecimal price24) {
|
||||
|
||||
@Override
|
||||
public LocalDateTime validTo() {
|
||||
return Objects.isNull(validTo) ? LocalDateTime.MAX : validTo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price2() {
|
||||
return Objects.requireNonNullElse(price2, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price3() {
|
||||
return Objects.requireNonNullElse(price3, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price4() {
|
||||
return Objects.requireNonNullElse(price4, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price5() {
|
||||
return Objects.requireNonNullElse(price5, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price6() {
|
||||
return Objects.requireNonNullElse(price6, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price7() {
|
||||
return Objects.requireNonNullElse(price7, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price8() {
|
||||
return Objects.requireNonNullElse(price8, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price9() {
|
||||
return Objects.requireNonNullElse(price9, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price10() {
|
||||
return Objects.requireNonNullElse(price10, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price11() {
|
||||
return Objects.requireNonNullElse(price11, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price12() {
|
||||
return Objects.requireNonNullElse(price12, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price13() {
|
||||
return Objects.requireNonNullElse(price13, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price14() {
|
||||
return Objects.requireNonNullElse(price14, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price15() {
|
||||
return Objects.requireNonNullElse(price15, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price16() {
|
||||
return Objects.requireNonNullElse(price16, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price17() {
|
||||
return Objects.requireNonNullElse(price17, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price18() {
|
||||
return Objects.requireNonNullElse(price18, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price19() {
|
||||
return Objects.requireNonNullElse(price19, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price20() {
|
||||
return Objects.requireNonNullElse(price20, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price21() {
|
||||
return Objects.requireNonNullElse(price21, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price22() {
|
||||
return Objects.requireNonNullElse(price22, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price23() {
|
||||
return Objects.requireNonNullElse(price23, price1());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal price24() {
|
||||
return Objects.requireNonNullElse(price24, price1());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Map} of tariffs with hour start as key.
|
||||
*
|
||||
* @return map with hourly tariffs
|
||||
*/
|
||||
public Map<LocalTime, BigDecimal> getTariffMap() {
|
||||
Map<LocalTime, BigDecimal> tariffMap = new HashMap<>();
|
||||
|
||||
tariffMap.put(LocalTime.of(0, 0), price1());
|
||||
tariffMap.put(LocalTime.of(1, 0), price2());
|
||||
tariffMap.put(LocalTime.of(2, 0), price3());
|
||||
tariffMap.put(LocalTime.of(3, 0), price4());
|
||||
tariffMap.put(LocalTime.of(4, 0), price5());
|
||||
tariffMap.put(LocalTime.of(5, 0), price6());
|
||||
tariffMap.put(LocalTime.of(6, 0), price7());
|
||||
tariffMap.put(LocalTime.of(7, 0), price8());
|
||||
tariffMap.put(LocalTime.of(8, 0), price9());
|
||||
tariffMap.put(LocalTime.of(9, 0), price10());
|
||||
tariffMap.put(LocalTime.of(10, 0), price11());
|
||||
tariffMap.put(LocalTime.of(11, 0), price12());
|
||||
tariffMap.put(LocalTime.of(12, 0), price13());
|
||||
tariffMap.put(LocalTime.of(13, 0), price14());
|
||||
tariffMap.put(LocalTime.of(14, 0), price15());
|
||||
tariffMap.put(LocalTime.of(15, 0), price16());
|
||||
tariffMap.put(LocalTime.of(16, 0), price17());
|
||||
tariffMap.put(LocalTime.of(17, 0), price18());
|
||||
tariffMap.put(LocalTime.of(18, 0), price19());
|
||||
tariffMap.put(LocalTime.of(19, 0), price20());
|
||||
tariffMap.put(LocalTime.of(20, 0), price21());
|
||||
tariffMap.put(LocalTime.of(21, 0), price22());
|
||||
tariffMap.put(LocalTime.of(22, 0), price23());
|
||||
tariffMap.put(LocalTime.of(23, 0), price24());
|
||||
|
||||
return tariffMap;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
@ -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) {
|
||||
}
|
@ -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) {
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api.serialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link InstantDeserializer} converts a formatted UTC string to {@link Instant}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class InstantDeserializer implements JsonDeserializer<Instant> {
|
||||
|
||||
@Override
|
||||
public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
|
||||
throws JsonParseException {
|
||||
String content = element.getAsString();
|
||||
try {
|
||||
// When writing this, the format of the provided UTC strings lacks the trailing 'Z'.
|
||||
// In case this would be fixed in the future, gracefully support both with and without this.
|
||||
return Instant.parse(content.endsWith("Z") ? content : content + "Z");
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new JsonParseException("Could not parse as Instant: " + content, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api.serialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link LocalDateDeserializer} converts a formatted string to {@link LocalDate}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocalDateDeserializer implements JsonDeserializer<LocalDate> {
|
||||
|
||||
@Override
|
||||
public @Nullable LocalDate deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
|
||||
throws JsonParseException {
|
||||
try {
|
||||
return LocalDate.parse(element.getAsString().substring(0, 10));
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new JsonParseException("Could not parse as LocalDate: " + element.getAsString(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api.serialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link LocalDateTimeDeserializer} converts a formatted string to {@link LocalDateTime}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocalDateTimeDeserializer implements JsonDeserializer<LocalDateTime> {
|
||||
|
||||
@Override
|
||||
public @Nullable LocalDateTime deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
|
||||
throws JsonParseException {
|
||||
try {
|
||||
return LocalDateTime.parse(element.getAsString());
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new JsonParseException("Could not parse as LocalDateTime: " + element.getAsString(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.config;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
|
||||
import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
|
||||
import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
|
||||
|
||||
/**
|
||||
* The {@link DatahubPriceConfiguration} class contains fields mapping channel configuration parameters.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DatahubPriceConfiguration {
|
||||
|
||||
/**
|
||||
* Comma-separated list of charge type codes, e.g. "CD,CD R".
|
||||
*/
|
||||
public String chargeTypeCodes = "";
|
||||
|
||||
/**
|
||||
* Comma-separated list of notes, e.g. "Nettarif C".
|
||||
*/
|
||||
public String notes = "";
|
||||
|
||||
/**
|
||||
* Query start date parameter expressed as either yyyy-mm-dd or one of StartOfDay, StartOfMonth or StartOfYear.
|
||||
*/
|
||||
public String start = "";
|
||||
|
||||
/**
|
||||
* Check if any filter values are provided.
|
||||
*
|
||||
* @return true if either charge type codes, notes or query start date is provided.
|
||||
*/
|
||||
public boolean hasAnyFilterOverrides() {
|
||||
return !chargeTypeCodes.isBlank() || !notes.isBlank() || !start.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed set of charge type codes from comma-separated string.
|
||||
*
|
||||
* @return Set of charge type codes.
|
||||
*/
|
||||
public Set<ChargeTypeCode> getChargeTypeCodes() {
|
||||
return chargeTypeCodes.isBlank() ? new HashSet<>()
|
||||
: new HashSet<ChargeTypeCode>(
|
||||
Arrays.stream(chargeTypeCodes.split(",")).map(ChargeTypeCode::new).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed set of notes from comma-separated string.
|
||||
*
|
||||
* @return Set of notes.
|
||||
*/
|
||||
public Set<String> getNotes() {
|
||||
return notes.isBlank() ? new HashSet<>() : new HashSet<String>(Arrays.asList(notes.split(",")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query start parameter.
|
||||
*
|
||||
* @return null if invalid, otherwise an initialized {@link DateQueryParameter}.
|
||||
*/
|
||||
public @Nullable DateQueryParameter getStart() {
|
||||
if (start.isBlank()) {
|
||||
return DateQueryParameter.EMPTY;
|
||||
}
|
||||
if (start.equals(DateQueryParameterType.START_OF_DAY.toString())) {
|
||||
return DateQueryParameter.of(DateQueryParameterType.START_OF_DAY);
|
||||
}
|
||||
if (start.equals(DateQueryParameterType.START_OF_MONTH.toString())) {
|
||||
return DateQueryParameter.of(DateQueryParameterType.START_OF_MONTH);
|
||||
}
|
||||
if (start.equals(DateQueryParameterType.START_OF_YEAR.toString())) {
|
||||
return DateQueryParameter.of(DateQueryParameterType.START_OF_YEAR);
|
||||
}
|
||||
try {
|
||||
return DateQueryParameter.of(LocalDate.parse(start));
|
||||
} catch (DateTimeParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.factory;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link EnergiDataServiceHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.energidataservice", service = ThingHandlerFactory.class)
|
||||
public class EnergiDataServiceHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERVICE);
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
@Activate
|
||||
public EnergiDataServiceHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_SERVICE.equals(thingTypeUID)) {
|
||||
return new EnergiDataServiceHandler(thing, httpClient, timeZoneProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.handler;
|
||||
|
||||
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.energidataservice.internal.ApiController;
|
||||
import org.openhab.binding.energidataservice.internal.CacheManager;
|
||||
import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions;
|
||||
import org.openhab.binding.energidataservice.internal.api.ChargeType;
|
||||
import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
|
||||
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
|
||||
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory;
|
||||
import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
|
||||
import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
|
||||
import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
|
||||
import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration;
|
||||
import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration;
|
||||
import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
|
||||
import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory;
|
||||
import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link EnergiDataServiceHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EnergiDataServiceHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class);
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
private final ApiController apiController;
|
||||
private final CacheManager cacheManager;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private EnergiDataServiceConfiguration config;
|
||||
private RetryStrategy retryPolicy = RetryPolicyFactory.initial();
|
||||
private @Nullable ScheduledFuture<?> refreshFuture;
|
||||
private @Nullable ScheduledFuture<?> priceUpdateFuture;
|
||||
|
||||
private record Price(String hourStart, BigDecimal spotPrice, String spotPriceCurrency,
|
||||
@Nullable BigDecimal netTariff, @Nullable BigDecimal systemTariff, @Nullable BigDecimal electricityTax,
|
||||
@Nullable BigDecimal transmissionNetTariff) {
|
||||
}
|
||||
|
||||
public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
|
||||
super(thing);
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
this.apiController = new ApiController(httpClient, timeZoneProvider);
|
||||
this.cacheManager = new CacheManager();
|
||||
|
||||
// Default configuration
|
||||
this.config = new EnergiDataServiceConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (!(command instanceof RefreshType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ELECTRICITY_CHANNELS.contains(channelUID.getId())) {
|
||||
refreshElectricityPrices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(EnergiDataServiceConfiguration.class);
|
||||
|
||||
if (config.priceArea.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.no-price-area");
|
||||
return;
|
||||
}
|
||||
GlobalLocationNumber gln = config.getGridCompanyGLN();
|
||||
if (!gln.isEmpty() && !gln.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.invalid-grid-company-gln");
|
||||
return;
|
||||
}
|
||||
gln = config.getEnerginetGLN();
|
||||
if (!gln.isEmpty() && !gln.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.invalid-energinet-gln");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
refreshFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> refreshFuture = this.refreshFuture;
|
||||
if (refreshFuture != null) {
|
||||
refreshFuture.cancel(true);
|
||||
this.refreshFuture = null;
|
||||
}
|
||||
ScheduledFuture<?> priceUpdateFuture = this.priceUpdateFuture;
|
||||
if (priceUpdateFuture != null) {
|
||||
priceUpdateFuture.cancel(true);
|
||||
this.priceUpdateFuture = null;
|
||||
}
|
||||
|
||||
cacheManager.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(EnergiDataServiceActions.class);
|
||||
}
|
||||
|
||||
private void refreshElectricityPrices() {
|
||||
RetryStrategy retryPolicy;
|
||||
try {
|
||||
if (isLinked(CHANNEL_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
downloadSpotPrices();
|
||||
}
|
||||
|
||||
if (isLinked(CHANNEL_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
downloadNetTariffs();
|
||||
}
|
||||
|
||||
if (isLinked(CHANNEL_SYSTEM_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
downloadSystemTariffs();
|
||||
}
|
||||
|
||||
if (isLinked(CHANNEL_ELECTRICITY_TAX) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
downloadElectricityTaxes();
|
||||
}
|
||||
|
||||
if (isLinked(CHANNEL_TRANSMISSION_NET_TARIFF) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
downloadTransmissionNetTariffs();
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updatePrices();
|
||||
|
||||
if (isLinked(CHANNEL_SPOT_PRICE) || isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
if (cacheManager.getNumberOfFutureSpotPrices() < 13) {
|
||||
retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing(DAILY_REFRESH_TIME_CET,
|
||||
NORD_POOL_TIMEZONE);
|
||||
} else {
|
||||
retryPolicy = RetryPolicyFactory.atFixedTime(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE);
|
||||
}
|
||||
} else {
|
||||
retryPolicy = RetryPolicyFactory.atFixedTime(LocalTime.MIDNIGHT, timeZoneProvider.getTimeZone());
|
||||
}
|
||||
} catch (DataServiceException e) {
|
||||
if (e.getHttpStatus() != 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
HttpStatus.getCode(e.getHttpStatus()).getMessage());
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
if (e.getCause() != null) {
|
||||
logger.debug("Error retrieving prices", e);
|
||||
}
|
||||
retryPolicy = RetryPolicyFactory.fromThrowable(e);
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("Refresh job interrupted");
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
rescheduleRefreshJob(retryPolicy);
|
||||
}
|
||||
|
||||
private void downloadSpotPrices() throws InterruptedException, DataServiceException {
|
||||
if (cacheManager.areSpotPricesFullyCached()) {
|
||||
logger.debug("Cached spot prices still valid, skipping download.");
|
||||
return;
|
||||
}
|
||||
DateQueryParameter start;
|
||||
if (cacheManager.areHistoricSpotPricesCached()) {
|
||||
start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW);
|
||||
} else {
|
||||
start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
|
||||
Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS));
|
||||
}
|
||||
Map<String, String> properties = editProperties();
|
||||
ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(),
|
||||
start, properties);
|
||||
cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency());
|
||||
updateProperties(properties);
|
||||
}
|
||||
|
||||
private void downloadNetTariffs() throws InterruptedException, DataServiceException {
|
||||
if (config.getGridCompanyGLN().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (cacheManager.areNetTariffsValidTomorrow()) {
|
||||
logger.debug("Cached net tariffs still valid, skipping download.");
|
||||
cacheManager.updateNetTariffs();
|
||||
} else {
|
||||
cacheManager.putNetTariffs(downloadPriceLists(config.getGridCompanyGLN(), getNetTariffFilter()));
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadSystemTariffs() throws InterruptedException, DataServiceException {
|
||||
GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
|
||||
if (globalLocationNumber.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (cacheManager.areSystemTariffsValidTomorrow()) {
|
||||
logger.debug("Cached system tariffs still valid, skipping download.");
|
||||
cacheManager.updateSystemTariffs();
|
||||
} else {
|
||||
cacheManager.putSystemTariffs(
|
||||
downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getSystemTariff()));
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadElectricityTaxes() throws InterruptedException, DataServiceException {
|
||||
GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
|
||||
if (globalLocationNumber.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (cacheManager.areElectricityTaxesValidTomorrow()) {
|
||||
logger.debug("Cached electricity taxes still valid, skipping download.");
|
||||
cacheManager.updateElectricityTaxes();
|
||||
} else {
|
||||
cacheManager.putElectricityTaxes(
|
||||
downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getElectricityTax()));
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadTransmissionNetTariffs() throws InterruptedException, DataServiceException {
|
||||
GlobalLocationNumber globalLocationNumber = config.getEnerginetGLN();
|
||||
if (globalLocationNumber.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (cacheManager.areTransmissionNetTariffsValidTomorrow()) {
|
||||
logger.debug("Cached transmission net tariffs still valid, skipping download.");
|
||||
cacheManager.updateTransmissionNetTariffs();
|
||||
} else {
|
||||
cacheManager.putTransmissionNetTariffs(
|
||||
downloadPriceLists(globalLocationNumber, DatahubTariffFilterFactory.getTransmissionNetTariff()));
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber,
|
||||
DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
|
||||
Map<String, String> properties = editProperties();
|
||||
Collection<DatahubPricelistRecord> records = apiController.getDatahubPriceLists(globalLocationNumber,
|
||||
ChargeType.Tariff, filter, properties);
|
||||
updateProperties(properties);
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private DatahubTariffFilter getNetTariffFilter() {
|
||||
Channel channel = getThing().getChannel(CHANNEL_NET_TARIFF);
|
||||
if (channel == null) {
|
||||
return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
|
||||
}
|
||||
|
||||
DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration()
|
||||
.as(DatahubPriceConfiguration.class);
|
||||
|
||||
if (!datahubPriceConfiguration.hasAnyFilterOverrides()) {
|
||||
return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
|
||||
}
|
||||
|
||||
DateQueryParameter start = datahubPriceConfiguration.getStart();
|
||||
if (start == null) {
|
||||
logger.warn("Invalid channel configuration parameter 'start': {}", datahubPriceConfiguration.start);
|
||||
return DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN);
|
||||
}
|
||||
|
||||
Set<ChargeTypeCode> chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes();
|
||||
Set<String> notes = datahubPriceConfiguration.getNotes();
|
||||
if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) {
|
||||
// Completely override filter.
|
||||
return new DatahubTariffFilter(chargeTypeCodes, notes, start);
|
||||
} else {
|
||||
// Only override start date in pre-configured filter.
|
||||
return new DatahubTariffFilter(DatahubTariffFilterFactory.getNetTariffByGLN(config.gridCompanyGLN), start);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePrices() {
|
||||
cacheManager.cleanup();
|
||||
|
||||
updateCurrentSpotPrice();
|
||||
updateCurrentTariff(CHANNEL_NET_TARIFF, cacheManager.getNetTariff());
|
||||
updateCurrentTariff(CHANNEL_SYSTEM_TARIFF, cacheManager.getSystemTariff());
|
||||
updateCurrentTariff(CHANNEL_ELECTRICITY_TAX, cacheManager.getElectricityTax());
|
||||
updateCurrentTariff(CHANNEL_TRANSMISSION_NET_TARIFF, cacheManager.getTransmissionNetTariff());
|
||||
updateHourlyPrices();
|
||||
|
||||
reschedulePriceUpdateJob();
|
||||
}
|
||||
|
||||
private void updateCurrentSpotPrice() {
|
||||
if (!isLinked(CHANNEL_SPOT_PRICE)) {
|
||||
return;
|
||||
}
|
||||
BigDecimal spotPrice = cacheManager.getSpotPrice();
|
||||
updateState(CHANNEL_SPOT_PRICE, spotPrice != null ? new DecimalType(spotPrice) : UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) {
|
||||
if (!isLinked(channelId)) {
|
||||
return;
|
||||
}
|
||||
updateState(channelId, tariff != null ? new DecimalType(tariff) : UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
private void updateHourlyPrices() {
|
||||
if (!isLinked(CHANNEL_HOURLY_PRICES)) {
|
||||
return;
|
||||
}
|
||||
Map<Instant, BigDecimal> spotPriceMap = cacheManager.getSpotPrices();
|
||||
Price[] targetPrices = new Price[spotPriceMap.size()];
|
||||
List<Entry<Instant, BigDecimal>> sourcePrices = spotPriceMap.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey()).toList();
|
||||
|
||||
int i = 0;
|
||||
for (Entry<Instant, BigDecimal> sourcePrice : sourcePrices) {
|
||||
Instant hourStart = sourcePrice.getKey();
|
||||
BigDecimal netTariff = cacheManager.getNetTariff(hourStart);
|
||||
BigDecimal systemTariff = cacheManager.getSystemTariff(hourStart);
|
||||
BigDecimal electricityTax = cacheManager.getElectricityTax(hourStart);
|
||||
BigDecimal transmissionNetTariff = cacheManager.getTransmissionNetTariff(hourStart);
|
||||
targetPrices[i++] = new Price(hourStart.toString(), sourcePrice.getValue(), config.currencyCode, netTariff,
|
||||
systemTariff, electricityTax, transmissionNetTariff);
|
||||
}
|
||||
updateState(CHANNEL_HOURLY_PRICES, new StringType(gson.toJson(targetPrices)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link Currency} for spot prices.
|
||||
*
|
||||
* @return Spot price currency
|
||||
*/
|
||||
public Currency getCurrency() {
|
||||
return config.getCurrency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached spot prices or try once to download them if not cached
|
||||
* (usually if no items are linked).
|
||||
*
|
||||
* @return Map of future spot prices
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getSpotPrices() {
|
||||
try {
|
||||
downloadSpotPrices();
|
||||
} catch (DataServiceException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Error retrieving spot prices", e);
|
||||
} else {
|
||||
logger.warn("Error retrieving spot prices: {}", e.getMessage());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return cacheManager.getSpotPrices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached net tariffs or try once to download them if not cached
|
||||
* (usually if no items are linked).
|
||||
*
|
||||
* @return Map of future net tariffs
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getNetTariffs() {
|
||||
try {
|
||||
downloadNetTariffs();
|
||||
} catch (DataServiceException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Error retrieving net tariffs", e);
|
||||
} else {
|
||||
logger.warn("Error retrieving net tariffs: {}", e.getMessage());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return cacheManager.getNetTariffs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached system tariffs or try once to download them if not cached
|
||||
* (usually if no items are linked).
|
||||
*
|
||||
* @return Map of future system tariffs
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getSystemTariffs() {
|
||||
try {
|
||||
downloadSystemTariffs();
|
||||
} catch (DataServiceException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Error retrieving system tariffs", e);
|
||||
} else {
|
||||
logger.warn("Error retrieving system tariffs: {}", e.getMessage());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return cacheManager.getSystemTariffs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached electricity taxes or try once to download them if not cached
|
||||
* (usually if no items are linked).
|
||||
*
|
||||
* @return Map of future electricity taxes
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getElectricityTaxes() {
|
||||
try {
|
||||
downloadElectricityTaxes();
|
||||
} catch (DataServiceException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Error retrieving electricity taxes", e);
|
||||
} else {
|
||||
logger.warn("Error retrieving electricity taxes: {}", e.getMessage());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return cacheManager.getElectricityTaxes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cached transmission net tariffs or try once to download them if not cached
|
||||
* (usually if no items are linked).
|
||||
*
|
||||
* @return Map of future transmissions net tariffs
|
||||
*/
|
||||
public Map<Instant, BigDecimal> getTransmissionNetTariffs() {
|
||||
try {
|
||||
downloadTransmissionNetTariffs();
|
||||
} catch (DataServiceException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Error retrieving transmission net tariffs", e);
|
||||
} else {
|
||||
logger.warn("Error retrieving transmission net tariffs: {}", e.getMessage());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return cacheManager.getTransmissionNetTariffs();
|
||||
}
|
||||
|
||||
private void reschedulePriceUpdateJob() {
|
||||
ScheduledFuture<?> priceUpdateJob = this.priceUpdateFuture;
|
||||
if (priceUpdateJob != null) {
|
||||
// Do not interrupt ourselves.
|
||||
priceUpdateJob.cancel(false);
|
||||
this.priceUpdateFuture = null;
|
||||
}
|
||||
|
||||
Instant now = Instant.now();
|
||||
long millisUntilNextClockHour = Duration
|
||||
.between(now, now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS)).toMillis() + 1;
|
||||
this.priceUpdateFuture = scheduler.schedule(this::updatePrices, millisUntilNextClockHour,
|
||||
TimeUnit.MILLISECONDS);
|
||||
logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour);
|
||||
}
|
||||
|
||||
private void rescheduleRefreshJob(RetryStrategy retryPolicy) {
|
||||
// Preserve state of previous retry policy when configuration is the same.
|
||||
if (!retryPolicy.equals(this.retryPolicy)) {
|
||||
this.retryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
ScheduledFuture<?> refreshJob = this.refreshFuture;
|
||||
|
||||
long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds();
|
||||
Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh);
|
||||
this.refreshFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh,
|
||||
TimeUnit.SECONDS);
|
||||
logger.debug("Refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
|
||||
updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone())
|
||||
.truncatedTo(ChronoUnit.SECONDS).format(formatter));
|
||||
|
||||
if (refreshJob != null) {
|
||||
refreshJob.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="energidataservice" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Energi Data Service Binding</name>
|
||||
<description>This is the binding for Energi Data Service providing open energy data from Energinet.</description>
|
||||
<connection>cloud</connection>
|
||||
<countries>dk,no,se</countries>
|
||||
</addon:addon>
|
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:energidataservice:service">
|
||||
<parameter name="priceArea" type="text" required="true">
|
||||
<label>Price Area</label>
|
||||
<description>Price area for spot prices (same as bidding zone).</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<options>
|
||||
<option value="DK1">West of the Great Belt</option>
|
||||
<option value="DK2">East of the Great Belt</option>
|
||||
</options>
|
||||
</parameter>
|
||||
<parameter name="currencyCode" type="text">
|
||||
<label>Currency Code</label>
|
||||
<description>Currency code in which to obtain spot prices.</description>
|
||||
<default>DKK</default>
|
||||
<options>
|
||||
<option value="DKK">Danish Krone</option>
|
||||
<option value="EUR">Euro</option>
|
||||
</options>
|
||||
</parameter>
|
||||
<parameter name="gridCompanyGLN" type="text">
|
||||
<label>Grid Company GLN</label>
|
||||
<description>Global Location Number of the grid company.</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<options>
|
||||
<option value="5790000705184">Cerius</option>
|
||||
<option value="5790000610099">Dinel</option>
|
||||
<option value="5790002502699">El-net Kongerslev</option>
|
||||
<option value="5790000836239">Elektrus</option>
|
||||
<option value="5790001095277">Elinord</option>
|
||||
<option value="5790001100520">Elnet Midt</option>
|
||||
<option value="5790000392551">FLOW Elnet</option>
|
||||
<option value="5790001090166">Hammel Elforsyning Net</option>
|
||||
<option value="5790000610839">Hurup Elværk Net</option>
|
||||
<option value="5790000682102">Ikast El Net</option>
|
||||
<option value="5790000704842">Konstant</option>
|
||||
<option value="5790001090111">L-Net</option>
|
||||
<option value="5790001089023">Midtfyns Elforsyning</option>
|
||||
<option value="5790001089030">N1</option>
|
||||
<option value="5790000681075">Netselskabet Elværk</option>
|
||||
<option value="5790001088231">NKE-Elnet</option>
|
||||
<option value="5790000610877">Nord Energi Net</option>
|
||||
<option value="5790000395620">Nordvestjysk Elforsyning (NOE Net)</option>
|
||||
<option value="5790000705689">Radius</option>
|
||||
<option value="5790000681327">RAH</option>
|
||||
<option value="5790000836727">Ravdex</option>
|
||||
<option value="5790001095444">Sunds Net</option>
|
||||
<option value="5790000706419">Tarm Elværk Net</option>
|
||||
<option value="5790000392261">TREFOR El-net</option>
|
||||
<option value="5790000706686">TREFOR El-net Øst</option>
|
||||
<option value="5790001088217">Veksel</option>
|
||||
<option value="5790000610976">Vores Elnet</option>
|
||||
<option value="5790001089375">Zeanet</option>
|
||||
</options>
|
||||
</parameter>
|
||||
<parameter name="energinetGLN" type="text">
|
||||
<label>Energinet GLN</label>
|
||||
<description>Global Location Number of Energinet.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>5790000432752</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="channel-type:energidataservice:datahub-price">
|
||||
<parameter name="chargeTypeCodes" type="text">
|
||||
<label>Charge Type Code Filters</label>
|
||||
<description>Comma-separated list of charge type codes.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="notes" type="text">
|
||||
<label>Note Filters</label>
|
||||
<description>Comma-separated list of notes.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="start" type="text">
|
||||
<label>Query Start Date</label>
|
||||
<description>Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay,
|
||||
StartOfMonth or StartOfYear.</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<options>
|
||||
<option value="StartOfDay">Start of day</option>
|
||||
<option value="StartOfMonth">Start of month</option>
|
||||
<option value="StartOfYear">Start of year</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
@ -0,0 +1,118 @@
|
||||
# add-on
|
||||
|
||||
addon.energidataservice.name = Energi Data Service Binding
|
||||
addon.energidataservice.description = This is the binding for Energi Data Service providing open energy data from Energinet.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.energidataservice.service.label = Energi Data Service
|
||||
thing-type.energidataservice.service.description = This Thing represents the Energi Data Service API.
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.energidataservice.service.currencyCode.label = Currency Code
|
||||
thing-type.config.energidataservice.service.currencyCode.description = Currency code in which to obtain spot prices.
|
||||
thing-type.config.energidataservice.service.currencyCode.option.DKK = Danish Krone
|
||||
thing-type.config.energidataservice.service.currencyCode.option.EUR = Euro
|
||||
thing-type.config.energidataservice.service.energinetGLN.label = Energinet GLN
|
||||
thing-type.config.energidataservice.service.energinetGLN.description = Global Location Number of Energinet.
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.label = Grid Company GLN
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.description = Global Location Number of the grid company.
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705184 = Cerius
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610099 = Dinel
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790002502699 = El-net Kongerslev
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836239 = Elektrus
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001095277 = Elinord
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001100520 = Elnet Midt
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392551 = FLOW Elnet
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090166 = Hammel Elforsyning Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610839 = Hurup Elværk Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000682102 = Ikast El Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000704842 = Konstant
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001090111 = L-Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089023 = Midtfyns Elforsyning
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089030 = N1
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681075 = Netselskabet Elværk
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088231 = NKE-Elnet
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610877 = Nord Energi Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000395620 = Nordvestjysk Elforsyning (NOE Net)
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000705689 = Radius
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000681327 = RAH
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000836727 = Ravdex
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001095444 = Sunds Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706419 = Tarm Elværk Net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000392261 = TREFOR El-net
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706686 = TREFOR El-net Øst
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088217 = Veksel
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610976 = Vores Elnet
|
||||
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089375 = Zeanet
|
||||
thing-type.config.energidataservice.service.priceArea.label = Price Area
|
||||
thing-type.config.energidataservice.service.priceArea.description = Price area for spot prices (same as bidding zone).
|
||||
thing-type.config.energidataservice.service.priceArea.option.DK1 = West of the Great Belt
|
||||
thing-type.config.energidataservice.service.priceArea.option.DK2 = East of the Great Belt
|
||||
|
||||
# channel group types
|
||||
|
||||
channel-group-type.energidataservice.electricity.label = Electricity
|
||||
channel-group-type.energidataservice.electricity.description = Channels related to electricity
|
||||
channel-group-type.energidataservice.electricity.channel.electricity-tax.label = Electricity Tax
|
||||
channel-group-type.energidataservice.electricity.channel.electricity-tax.description = Current electricity tax in DKK per kWh.
|
||||
channel-group-type.energidataservice.electricity.channel.net-tariff.label = Net Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.net-tariff.description = Current net tariff in DKK per kWh.
|
||||
channel-group-type.energidataservice.electricity.channel.spot-price.label = Spot Price
|
||||
channel-group-type.energidataservice.electricity.channel.spot-price.description = Current spot price in DKK or EUR per kWh.
|
||||
channel-group-type.energidataservice.electricity.channel.system-tariff.label = System Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.system-tariff.description = Current system tariff in DKK per kWh.
|
||||
channel-group-type.energidataservice.electricity.channel.transmission-net-tariff.label = Transmission Net Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.transmission-net-tariff.description = Current transmission net tariff in DKK per kWh.
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.energidataservice.datahub-price.label = Datahub Price
|
||||
channel-type.energidataservice.datahub-price.description = Datahub price.
|
||||
channel-type.energidataservice.hourly-prices.label = Hourly Prices
|
||||
channel-type.energidataservice.hourly-prices.description = JSON array with hourly prices from 12 hours ago and onward.
|
||||
channel-type.energidataservice.spot-price.label = Spot Price
|
||||
channel-type.energidataservice.spot-price.description = Spot price.
|
||||
|
||||
# channel types config
|
||||
|
||||
channel-type.config.energidataservice.datahub-price.chargeTypeCodes.label = Charge Type Code Filters
|
||||
channel-type.config.energidataservice.datahub-price.chargeTypeCodes.description = Comma-separated list of charge type codes.
|
||||
channel-type.config.energidataservice.datahub-price.notes.label = Note Filters
|
||||
channel-type.config.energidataservice.datahub-price.notes.description = Comma-separated list of notes.
|
||||
channel-type.config.energidataservice.datahub-price.start.label = Query Start Date
|
||||
channel-type.config.energidataservice.datahub-price.start.description = Query start date parameter expressed as either YYYY-MM-DD or dynamically as one of StartOfDay, StartOfMonth or StartOfYear.
|
||||
channel-type.config.energidataservice.datahub-price.start.option.StartOfDay = Start of day
|
||||
channel-type.config.energidataservice.datahub-price.start.option.StartOfMonth = Start of month
|
||||
channel-type.config.energidataservice.datahub-price.start.option.StartOfYear = Start of year
|
||||
|
||||
# channel group types
|
||||
|
||||
channel-group-type.energidataservice.electricity.channel.current-electricity-tax.label = Current Electricity Tax
|
||||
channel-group-type.energidataservice.electricity.channel.current-electricity-tax.description = Electricity Tax in DKK per kWh for current hour.
|
||||
channel-group-type.energidataservice.electricity.channel.current-net-tariff.label = Current Net Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.current-net-tariff.description = Net tariff in DKK per kWh for current hour.
|
||||
channel-group-type.energidataservice.electricity.channel.current-spot-price.label = Current Spot Price
|
||||
channel-group-type.energidataservice.electricity.channel.current-spot-price.description = Spot price in DKK or EUR per kWh for current hour.
|
||||
channel-group-type.energidataservice.electricity.channel.current-system-tariff.label = Current System Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.current-system-tariff.description = System tariff in DKK per kWh for current hour.
|
||||
channel-group-type.energidataservice.electricity.channel.current-transmission-net-tariff.label = Current Transmission Tariff
|
||||
channel-group-type.energidataservice.electricity.channel.current-transmission-net-tariff.description = Transmission Net Tariff in DKK per kWh for current hour.
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.conf-error.no-price-area = Price area must be set
|
||||
offline.conf-error.invalid-grid-company-gln = Invalid grid company GLN
|
||||
offline.conf-error.invalid-energinet-gln = Invalid Energinet GLN
|
||||
|
||||
# actions
|
||||
|
||||
action.calculate-cheapest-period.label = calculate cheapest period
|
||||
action.calculate-cheapest-period.description = calculate cheapest period for using power according to a supplied timetable (excl. VAT)
|
||||
action.calculate-price.label = calculate price
|
||||
action.calculate-price.description = calculate price for power consumption in period excl. VAT
|
||||
action.get-prices.label = get prices
|
||||
action.get-prices.description = get hourly prices excl. VAT
|
||||
action.get-prices.priceElements.label = price elements
|
||||
action.get-prices.priceElements.description = comma-separated list of price elements to include in sums
|
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="energidataservice"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-group-type id="electricity">
|
||||
<label>Electricity</label>
|
||||
<description>Channels related to electricity</description>
|
||||
<channels>
|
||||
<channel id="spot-price" typeId="spot-price">
|
||||
<label>Spot Price</label>
|
||||
<description>Current spot price in DKK or EUR per kWh.</description>
|
||||
</channel>
|
||||
<channel id="net-tariff" typeId="datahub-price">
|
||||
<label>Net Tariff</label>
|
||||
<description>Current net tariff in DKK per kWh.</description>
|
||||
</channel>
|
||||
<channel id="system-tariff" typeId="datahub-price">
|
||||
<label>System Tariff</label>
|
||||
<description>Current system tariff in DKK per kWh.</description>
|
||||
</channel>
|
||||
<channel id="electricity-tax" typeId="datahub-price">
|
||||
<label>Electricity Tax</label>
|
||||
<description>Current electricity tax in DKK per kWh.</description>
|
||||
</channel>
|
||||
<channel id="transmission-net-tariff" typeId="datahub-price">
|
||||
<label>Transmission Net Tariff</label>
|
||||
<description>Current transmission net tariff in DKK per kWh.</description>
|
||||
</channel>
|
||||
<channel id="hourly-prices" typeId="hourly-prices"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="energidataservice"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="spot-price">
|
||||
<item-type>Number</item-type>
|
||||
<label>Spot Price</label>
|
||||
<description>Spot price.</description>
|
||||
<category>Price</category>
|
||||
<state readOnly="true" pattern="%.9f"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="datahub-price">
|
||||
<item-type>Number</item-type>
|
||||
<label>Datahub Price</label>
|
||||
<description>Datahub price.</description>
|
||||
<category>Price</category>
|
||||
<state readOnly="true" pattern="%.6f"></state>
|
||||
<config-description-ref uri="channel-type:energidataservice:datahub-price"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="hourly-prices" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Hourly Prices</label>
|
||||
<description>JSON array with hourly prices from 12 hours ago and onward.</description>
|
||||
<category>Price</category>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="energidataservice"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="service">
|
||||
|
||||
<label>Energi Data Service</label>
|
||||
<description>This Thing represents the Energi Data Service API.</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="electricity" typeId="electricity"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description-ref uri="thing-type:energidataservice:service"/>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
|
||||
|
||||
/**
|
||||
* Tests for {@link CacheManager}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class CacheManagerTest {
|
||||
|
||||
@Test
|
||||
void areSpotPricesFullyCachedToday() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T08:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T22:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areSpotPricesFullyCachedTodayMissingAtStart() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T21:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T22:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areSpotPricesFullyCached(), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areSpotPricesFullyCachedTodayMissingAtEnd() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T20:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T21:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areSpotPricesFullyCached(), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areSpotPricesFullyCachedTodayOtherTimezoneIsIgnored() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T08:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T22:00:00Z");
|
||||
Clock clock = Clock.fixed(now, ZoneId.of("Asia/Tokyo"));
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areSpotPricesFullyCachedTomorrow() {
|
||||
Instant now = Instant.parse("2023-02-07T12:00:00Z");
|
||||
Instant first = Instant.parse("2023-02-06T12:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-08T22:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areSpotPricesFullyCached(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areHistoricSpotPricesCached() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T08:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T07:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areHistoricSpotPricesCached(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void areHistoricSpotPricesCachedFirstHourMissing() {
|
||||
Instant now = Instant.parse("2023-02-07T08:38:47Z");
|
||||
Instant first = Instant.parse("2023-02-06T21:00:00Z");
|
||||
Instant last = Instant.parse("2023-02-07T08:00:00Z");
|
||||
Clock clock = Clock.fixed(now, EnergiDataServiceBindingConstants.NORD_POOL_TIMEZONE);
|
||||
CacheManager cacheManager = new CacheManager(clock);
|
||||
populateWithSpotPrices(cacheManager, first, last);
|
||||
assertThat(cacheManager.areHistoricSpotPricesCached(), is(false));
|
||||
}
|
||||
|
||||
private void populateWithSpotPrices(CacheManager cacheManager, Instant first, Instant last) {
|
||||
int size = (int) Duration.between(first, last).getSeconds() / 60 / 60 + 1;
|
||||
ElspotpriceRecord[] records = new ElspotpriceRecord[size];
|
||||
int i = 0;
|
||||
for (Instant hourStart = first; !hourStart.isAfter(last); hourStart = hourStart.plus(1, ChronoUnit.HOURS)) {
|
||||
records[i++] = new ElspotpriceRecord(hourStart, BigDecimal.ONE, BigDecimal.ZERO);
|
||||
}
|
||||
cacheManager.putSpotPrices(records, EnergiDataServiceBindingConstants.CURRENCY_DKK);
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
|
||||
import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* Tests for {@link PriceListParser}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class PriceListParserTest {
|
||||
|
||||
private Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer())
|
||||
.create();
|
||||
|
||||
private <T> T getObjectFromJson(String filename, Class<T> clazz) throws IOException {
|
||||
try (InputStream inputStream = PriceListParserTest.class.getResourceAsStream(filename)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Input stream is null");
|
||||
}
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
if (bytes == null) {
|
||||
throw new IOException("Resulting byte-array empty");
|
||||
}
|
||||
String json = new String(bytes, StandardCharsets.UTF_8);
|
||||
return Objects.requireNonNull(gson.fromJson(json, clazz));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyNoChanges() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2023-01-23T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(60));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-23T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-23T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-24T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-24T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyNewTariffTomorrowWhenSummertime() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2023-03-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(60));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-03-31T14:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-03-31T15:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-04-01T14:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-04-01T15:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyNewTariffAtMidnight() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(), "CD");
|
||||
|
||||
assertThat(tariffMap.size(), is(60));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.407717"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyDiscount() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList(),
|
||||
"CD R");
|
||||
|
||||
assertThat(tariffMap.size(), is(60));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("-0.407717"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.0"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.0"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyTariffAndDiscountIsSum() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2022-11-30T15:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(57));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-11-30T15:00:00Z")), is(equalTo(new BigDecimal("0.387517"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-11-30T16:00:00Z")), is(equalTo(new BigDecimal("0.973404"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyTariffAndDiscountIsFree() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2022-12-31T12:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistN1.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(60));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T16:00:00Z")), is(equalTo(new BigDecimal("0.000000"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T22:00:00Z")), is(equalTo(new BigDecimal("0.000000"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2022-12-31T23:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-01T00:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyFixedTariff() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2022-12-31T23:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistNordEnergi.json",
|
||||
DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(25)); // No records in dataset before 2023-01-01
|
||||
for (Instant i = Instant.parse("2022-12-31T23:00:00Z"); i
|
||||
.isBefore(Instant.parse("2023-01-02T00:00:00Z")); i = i.plus(1, ChronoUnit.HOURS)) {
|
||||
assertThat(tariffMap.get(i), is(equalTo(new BigDecimal("0.245"))));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlyDailyTariffs() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2023-01-28T04:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistTrefor.json",
|
||||
DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(68));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-28T04:00:00Z")), is(equalTo(new BigDecimal("0.2581"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-28T05:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-28T16:00:00Z")), is(equalTo(new BigDecimal("2.3227"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-28T20:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-28T23:00:00Z")), is(equalTo(new BigDecimal("0.2581"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-29T05:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-29T16:00:00Z")), is(equalTo(new BigDecimal("2.3227"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-01-29T20:00:00Z")), is(equalTo(new BigDecimal("0.7742"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toHourlySystemTariff() throws IOException {
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(Instant.parse("2023-06-30T21:00:00Z"), EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords records = getObjectFromJson("DatahubPricelistElectricityTax.json",
|
||||
DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> tariffMap = priceListParser.toHourly(Arrays.stream(records.records()).toList());
|
||||
|
||||
assertThat(tariffMap.size(), is(51));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-06-30T21:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
|
||||
assertThat(tariffMap.get(Instant.parse("2023-06-30T22:00:00Z")), is(equalTo(new BigDecimal("0.697"))));
|
||||
}
|
||||
}
|
@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.action;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants;
|
||||
import org.openhab.binding.energidataservice.internal.PriceListParser;
|
||||
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
|
||||
import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
|
||||
import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
|
||||
import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
|
||||
/**
|
||||
* Tests for {@link EnergiDataServiceActions}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
public class EnergiDataServiceActionsTest {
|
||||
|
||||
private @NonNullByDefault({}) @Mock EnergiDataServiceHandler handler;
|
||||
private EnergiDataServiceActions actions = new EnergiDataServiceActions();
|
||||
|
||||
private Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer())
|
||||
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()).create();
|
||||
|
||||
private record SpotPrice(Instant hourStart, BigDecimal spotPrice) {
|
||||
}
|
||||
|
||||
private <T> T getObjectFromJson(String filename, Class<T> clazz) throws IOException {
|
||||
try (InputStream inputStream = EnergiDataServiceActionsTest.class.getResourceAsStream(filename)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Input stream is null");
|
||||
}
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
if (bytes == null) {
|
||||
throw new IOException("Resulting byte-array empty");
|
||||
}
|
||||
String json = new String(bytes, StandardCharsets.UTF_8);
|
||||
return Objects.requireNonNull(gson.fromJson(json, clazz));
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
final Logger logger = (Logger) LoggerFactory.getLogger(EnergiDataServiceActions.class);
|
||||
logger.setLevel(Level.OFF);
|
||||
|
||||
actions = new EnergiDataServiceActions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesSpotPrice() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice");
|
||||
assertThat(actual.size(), is(35));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.992840027"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.267680054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesNetTariff() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("NetTariff");
|
||||
assertThat(actual.size(), is(60));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesSystemTariff() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("SystemTariff");
|
||||
assertThat(actual.size(), is(60));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesElectricityTax() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("ElectricityTax");
|
||||
assertThat(actual.size(), is(60));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesTransmissionNetTariff() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("TransmissionNetTariff");
|
||||
assertThat(actual.size(), is(60));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesSpotPriceNetTariff() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice,NetTariff");
|
||||
assertThat(actual.size(), is(35));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.425065027"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.323870054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesSpotPriceNetTariffElectricityTax() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice,NetTariff,ElectricityTax");
|
||||
assertThat(actual.size(), is(35));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.433065027"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.331870054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesTotal() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices();
|
||||
assertThat(actual.size(), is(35));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesTotalAllElements() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions
|
||||
.getPrices("spotprice,nettariff,systemtariff,electricitytax,transmissionnettariff");
|
||||
assertThat(actual.size(), is(35));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T15:00:00Z")), is(equalTo(new BigDecimal("1.708765039"))));
|
||||
assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesInvalidPriceElement() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("spotprice,nettarif");
|
||||
assertThat(actual.size(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPricesMixedCurrencies() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_EUR);
|
||||
|
||||
Map<Instant, BigDecimal> actual = actions.getPrices("spotprice,nettariff");
|
||||
assertThat(actual.size(), is(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price in period 15:30-16:30 (UTC) with consumption 150 W and the following total prices:
|
||||
* 15:00:00: 1.708765039
|
||||
* 16:00:00: 2.443870054
|
||||
*
|
||||
* Result = (1.708765039 / 2) + (2.443870054 / 2) * 0.150
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
void calculatePriceSimple() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:30:00Z"),
|
||||
Instant.parse("2023-02-04T16:30:00Z"), new QuantityType<>(150, Units.WATT));
|
||||
assertThat(actual, is(equalTo(new BigDecimal("0.311447631975000000")))); // 0.3114476319750
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price in period 15:00-17:00 (UTC) with consumption 1000 W and the following total prices:
|
||||
* 15:00:00: 1.708765039
|
||||
* 16:00:00: 2.443870054
|
||||
*
|
||||
* Result = 1.708765039 + 2.443870054
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
void calculatePriceFullHours() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:00:00Z"),
|
||||
Instant.parse("2023-02-04T17:00:00Z"), new QuantityType<>(1, Units.KILOVAR));
|
||||
assertThat(actual, is(equalTo(new BigDecimal("4.152635093000000000")))); // 4.152635093
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculatePriceOutOfRangeStart() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-03T23:59:00Z"),
|
||||
Instant.parse("2023-02-04T12:30:00Z"), new QuantityType<>(1000, Units.WATT));
|
||||
assertThat(actual, is(equalTo(BigDecimal.ZERO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculatePriceOutOfRangeEnd() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-05T22:00:00Z"),
|
||||
Instant.parse("2023-02-05T23:01:00Z"), new QuantityType<>(1000, Units.WATT));
|
||||
assertThat(actual, is(equalTo(BigDecimal.ZERO)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
void calculateCheapestPeriodWithPowerDishwasher() throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230205.json");
|
||||
|
||||
List<Duration> durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
|
||||
Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41),
|
||||
Duration.ofMinutes(104));
|
||||
List<QuantityType<Power>> consumptions = List.of(QuantityType.valueOf(162.162162, Units.WATT),
|
||||
QuantityType.valueOf(750, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
|
||||
QuantityType.valueOf(3000, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
|
||||
QuantityType.valueOf(166.666666, Units.WATT), QuantityType.valueOf(146.341463, Units.WATT),
|
||||
QuantityType.valueOf(0, Units.WATT));
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
|
||||
Instant.parse("2023-02-06T06:00:00Z"), durations, consumptions);
|
||||
assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
|
||||
assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
|
||||
assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
|
||||
assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCheapestPeriodWithPowerOutOfRange() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
List<Duration> durations = List.of(Duration.ofMinutes(61));
|
||||
List<QuantityType<Power>> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT));
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
|
||||
Instant.parse("2023-02-06T00:01:00Z"), durations, consumptions);
|
||||
assertThat(actual.size(), is(equalTo(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
void calculateCheapestPeriodWithEnergyDishwasher() throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230205.json");
|
||||
|
||||
List<Duration> durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
|
||||
Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41));
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
|
||||
Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236), durations,
|
||||
QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
|
||||
assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
|
||||
assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
|
||||
assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
|
||||
assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCheapestPeriodWithEnergyTotalDurationIsExactSum() throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230205.json");
|
||||
|
||||
List<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
|
||||
Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(120), durations,
|
||||
QuantityType.valueOf(100, Units.WATT_HOUR));
|
||||
assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("0.293540001200000000"))));
|
||||
assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCheapestPeriodWithEnergyTotalDurationInvalid() throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230205.json");
|
||||
|
||||
List<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
|
||||
Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(119), durations,
|
||||
QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
|
||||
assertThat(actual.size(), is(equalTo(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #calculateCheapestPeriodWithEnergyDishwasher} but with unknown consumption/timetable map.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
void calculateCheapestPeriodAssumingLinearUnknownConsumption() throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230205.json");
|
||||
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
|
||||
Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236));
|
||||
assertThat(actual.get("LowestPrice"), is(nullValue()));
|
||||
assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
|
||||
assertThat(actual.get("HighestPrice"), is(nullValue()));
|
||||
assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCheapestPeriodForLinearPowerUsage() throws IOException {
|
||||
mockCommonDatasets(actions);
|
||||
|
||||
Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
|
||||
Instant.parse("2023-02-05T23:00:00Z"), Duration.ofMinutes(61), QuantityType.valueOf(1000, Units.WATT));
|
||||
assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.323990859575000000"))));
|
||||
assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T12:00:00Z"))));
|
||||
assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("2.589061780353348000"))));
|
||||
assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-04T17:00:00Z"))));
|
||||
}
|
||||
|
||||
private void mockCommonDatasets(EnergiDataServiceActions actions) throws IOException {
|
||||
mockCommonDatasets(actions, "SpotPrices20230204.json");
|
||||
}
|
||||
|
||||
private void mockCommonDatasets(EnergiDataServiceActions actions, String spotPricesFilename) throws IOException {
|
||||
SpotPrice[] spotPriceRecords = getObjectFromJson(spotPricesFilename, SpotPrice[].class);
|
||||
Map<Instant, BigDecimal> spotPrices = Arrays.stream(spotPriceRecords)
|
||||
.collect(Collectors.toMap(SpotPrice::hourStart, SpotPrice::spotPrice));
|
||||
|
||||
PriceListParser priceListParser = new PriceListParser(
|
||||
Clock.fixed(spotPriceRecords[0].hourStart, EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
|
||||
DatahubPricelistRecords datahubRecords = getObjectFromJson("NetTariffs.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> netTariffs = priceListParser
|
||||
.toHourly(Arrays.stream(datahubRecords.records()).toList());
|
||||
datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> systemTariffs = priceListParser
|
||||
.toHourly(Arrays.stream(datahubRecords.records()).toList());
|
||||
datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> electricityTaxes = priceListParser
|
||||
.toHourly(Arrays.stream(datahubRecords.records()).toList());
|
||||
datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class);
|
||||
Map<Instant, BigDecimal> transmissionNetTariffs = priceListParser
|
||||
.toHourly(Arrays.stream(datahubRecords.records()).toList());
|
||||
|
||||
when(handler.getSpotPrices()).thenReturn(spotPrices);
|
||||
when(handler.getNetTariffs()).thenReturn(netTariffs);
|
||||
when(handler.getSystemTariffs()).thenReturn(systemTariffs);
|
||||
when(handler.getElectricityTaxes()).thenReturn(electricityTaxes);
|
||||
when(handler.getTransmissionNetTariffs()).thenReturn(transmissionNetTariffs);
|
||||
when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_DKK);
|
||||
actions.setThingHandler(handler);
|
||||
}
|
||||
}
|
@ -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")));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.energidataservice.internal.api.serialization;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* Tests for {@link InstantDeserializer}.
|
||||
*
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class InstantDeserializerTest {
|
||||
|
||||
private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create();
|
||||
|
||||
@Test
|
||||
void instantWhenInvalidShouldThrowJsonParseException() {
|
||||
assertThrows(JsonParseException.class, () -> {
|
||||
gson.fromJson("\"invalid\"", Instant.class);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "\"2023-04-17T20:38:01Z\"", "\"2023-04-17T20:38:01\"" })
|
||||
void instantWhenValidShouldParse(String input) {
|
||||
assertThat((@Nullable Instant) gson.fromJson(input, Instant.class),
|
||||
is(equalTo(Instant.ofEpochSecond(1681763881))));
|
||||
}
|
||||
}
|
@ -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))));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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
|
||||
}
|
||||
]
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -125,6 +125,7 @@
|
||||
<module>org.openhab.binding.elerotransmitterstick</module>
|
||||
<module>org.openhab.binding.elroconnects</module>
|
||||
<module>org.openhab.binding.energenie</module>
|
||||
<module>org.openhab.binding.energidataservice</module>
|
||||
<module>org.openhab.binding.enigma2</module>
|
||||
<module>org.openhab.binding.enocean</module>
|
||||
<module>org.openhab.binding.enphase</module>
|
||||
|
Loading…
Reference in New Issue
Block a user