forked from openhab/openhab-addons
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[energidataservice] Initial contribution (openhab#14376)
* Initial contribution Signed-off-by: Jacob Laursen <[email protected]> * Remove Value-Added Tax Signed-off-by: Jacob Laursen <[email protected]> * Migrate naming convention Signed-off-by: Jacob Laursen <[email protected]> * Add channel configuration example Signed-off-by: Jacob Laursen <[email protected]> * Remove current prefixes for forward compatibility with timestamped items Signed-off-by: Jacob Laursen <[email protected]> * Add filter for another grid company Signed-off-by: Jacob Laursen <[email protected]> * Use ISO 3166-1 alpha-2 codes in lowercase for XSD compliance Signed-off-by: Jacob Laursen <[email protected]> * Fix error handling for deserializers Signed-off-by: Jacob Laursen <[email protected]> * Fix compliance with RFC 9110 section 10.1.5 Signed-off-by: Jacob Laursen <[email protected]> * Add JavaScript example code Signed-off-by: Jacob Laursen <[email protected]> * Refactor List to Collection and use iterators Signed-off-by: Jacob Laursen <[email protected]> * Add filter for another grid company Signed-off-by: Jacob Laursen <[email protected]> * Extend cached history to 24 hours Signed-off-by: Jacob Laursen <[email protected]> * Remove filter for expired GLN Signed-off-by: Jacob Laursen <[email protected]> * Fix typos Signed-off-by: Jacob Laursen <[email protected]> * Improve descriptions Signed-off-by: Jacob Laursen <[email protected]> * Improve logging Signed-off-by: Jacob Laursen <[email protected]> --------- Signed-off-by: Jacob Laursen <[email protected]> Signed-off-by: Matt Myers <[email protected]>
- Loading branch information
Showing
64 changed files
with
9,685 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
This content is produced and maintained by the openHAB project. | ||
|
||
* Project home: https://www.openhab.org | ||
|
||
== Declared Project Licenses | ||
|
||
This program and the accompanying materials are made available under the terms | ||
of the Eclipse Public License 2.0 which is available at | ||
https://www.eclipse.org/legal/epl-2.0/. | ||
|
||
== Source Code | ||
|
||
https://github.com/openhab/openhab-addons |
472 changes: 472 additions & 0 deletions
472
bundles/org.openhab.binding.energidataservice/README.md
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
9 changes: 9 additions & 0 deletions
9
bundles/org.openhab.binding.energidataservice/src/main/feature/feature.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
254 changes: 254 additions & 0 deletions
254
...taservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(",")) + "}"; | ||
} | ||
} |
Oops, something went wrong.