-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Jacob Laursen <[email protected]>
- Loading branch information
Showing
63 changed files
with
9,532 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 |
402 changes: 402 additions & 0 deletions
402
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.