[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:
Jacob Laursen 2023-07-03 18:16:17 +02:00 committed by GitHub
parent 4ddb3ce7e6
commit 6cfb1e295d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 9685 additions and 0 deletions

View File

@ -92,6 +92,7 @@
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.elroconnects/ @mherwege
/bundles/org.openhab.binding.energenie/ @hmerk
/bundles/org.openhab.binding.energidataservice/ @jlaur
/bundles/org.openhab.binding.enigma2/ @gdolfen
/bundles/org.openhab.binding.enocean/ @fruggy83
/bundles/org.openhab.binding.enphase/ @Hilbrand

View File

@ -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>

View 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

View 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"));
```
:::
::::

View 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>

View File

@ -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>

View File

@ -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(",")) + "}";
}
}

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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"))));
}
}

View File

@ -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);
}
}

View File

@ -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")));
}
}

View File

@ -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));
}
}

View File

@ -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))));
}
}

View File

@ -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))));
}
}

View File

@ -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));
}
}
}

View File

@ -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)));
}
}

View File

@ -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)));
}
}
}

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]

View File

@ -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
}
]

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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>