]> git.basschouten.com Git - openhab-addons.git/blob
c0bed681d00e656e5fa732ca44ab54112cd4f7c5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.energidataservice.internal;
14
15 import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
16
17 import java.time.Instant;
18 import java.time.LocalDateTime;
19 import java.time.format.DateTimeFormatter;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Currency;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
30 import java.util.stream.Collectors;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.http.HttpFields;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpStatus;
39 import org.openhab.binding.energidataservice.internal.api.ChargeType;
40 import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
41 import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
42 import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
43 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
44 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
45 import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
46 import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords;
47 import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
48 import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
49 import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
50 import org.openhab.core.i18n.TimeZoneProvider;
51 import org.osgi.framework.FrameworkUtil;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.Gson;
56 import com.google.gson.GsonBuilder;
57 import com.google.gson.JsonSyntaxException;
58
59 /**
60  * The {@link ApiController} is responsible for interacting with Energi Data Service.
61  *
62  * @author Jacob Laursen - Initial contribution
63  */
64 @NonNullByDefault
65 public class ApiController {
66     private static final String ENDPOINT = "https://api.energidataservice.dk/";
67     private static final String DATASET_PATH = "dataset/";
68
69     private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices";
70     private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist";
71
72     private static final String FILTER_KEY_PRICE_AREA = "PriceArea";
73     private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType";
74     private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode";
75     private static final String FILTER_KEY_GLN_NUMBER = "GLN_Number";
76     private static final String FILTER_KEY_NOTE = "Note";
77
78     private static final String HEADER_REMAINING_CALLS = "RemainingCalls";
79     private static final String HEADER_TOTAL_CALLS = "TotalCalls";
80     private static final int REQUEST_TIMEOUT_SECONDS = 30;
81
82     private final Logger logger = LoggerFactory.getLogger(ApiController.class);
83     private final Gson gson = new GsonBuilder() //
84             .registerTypeAdapter(Instant.class, new InstantDeserializer()) //
85             .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) //
86             .create();
87     private final HttpClient httpClient;
88     private final TimeZoneProvider timeZoneProvider;
89     private final String userAgent;
90
91     public ApiController(HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
92         this.httpClient = httpClient;
93         this.timeZoneProvider = timeZoneProvider;
94         userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
95     }
96
97     /**
98      * Retrieve spot prices for requested area and in requested {@link Currency}.
99      *
100      * @param priceArea Usually DK1 or DK2
101      * @param currency DKK or EUR
102      * @param start Specifies the start point of the period for the data request
103      * @param properties Map of properties which will be updated with metadata from headers
104      * @return Records with pairs of hour start and price in requested currency.
105      * @throws InterruptedException
106      * @throws DataServiceException
107      */
108     public ElspotpriceRecord[] getSpotPrices(String priceArea, Currency currency, DateQueryParameter start,
109             Map<String, String> properties) throws InterruptedException, DataServiceException {
110         if (!SUPPORTED_CURRENCIES.contains(currency)) {
111             throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode());
112         }
113
114         Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES)
115                 .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) //
116                 .param("start", start.toString()) //
117                 .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") //
118                 .param("columns", "HourUTC,SpotPrice" + currency) //
119                 .agent(userAgent) //
120                 .method(HttpMethod.GET);
121
122         logger.trace("GET request for {}", request.getURI());
123
124         try {
125             ContentResponse response = request.send();
126
127             updatePropertiesFromResponse(response, properties);
128
129             int status = response.getStatus();
130             if (!HttpStatus.isSuccess(status)) {
131                 throw new DataServiceException("The request failed with HTTP error " + status, status);
132             }
133             String responseContent = response.getContentAsString();
134             if (responseContent.isEmpty()) {
135                 throw new DataServiceException("Empty response");
136             }
137             logger.trace("Response content: '{}'", responseContent);
138
139             ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class);
140             if (records == null) {
141                 throw new DataServiceException("Error parsing response");
142             }
143
144             if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) {
145                 throw new DataServiceException("No records");
146             }
147
148             return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(ElspotpriceRecord[]::new);
149         } catch (JsonSyntaxException e) {
150             throw new DataServiceException("Error parsing response", e);
151         } catch (TimeoutException | ExecutionException e) {
152             throw new DataServiceException(e);
153         }
154     }
155
156     private void updatePropertiesFromResponse(ContentResponse response, Map<String, String> properties) {
157         HttpFields headers = response.getHeaders();
158         String remainingCalls = headers.get(HEADER_REMAINING_CALLS);
159         if (remainingCalls != null) {
160             properties.put(PROPERTY_REMAINING_CALLS, remainingCalls);
161         }
162         String totalCalls = headers.get(HEADER_TOTAL_CALLS);
163         if (totalCalls != null) {
164             properties.put(PROPERTY_TOTAL_CALLS, totalCalls);
165         }
166         DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
167         properties.put(PROPERTY_LAST_CALL, LocalDateTime.now(timeZoneProvider.getTimeZone()).format(formatter));
168     }
169
170     /**
171      * Retrieve datahub pricelists for requested GLN and charge type/charge type code.
172      *
173      * @param globalLocationNumber Global Location Number of the Charge Owner
174      * @param chargeType Charge type (Subscription, Fee or Tariff).
175      * @param tariffFilter Tariff filter (charge type codes and notes).
176      * @param properties Map of properties which will be updated with metadata from headers
177      * @return Price list for requested GLN and note.
178      * @throws InterruptedException
179      * @throws DataServiceException
180      */
181     public Collection<DatahubPricelistRecord> getDatahubPriceLists(GlobalLocationNumber globalLocationNumber,
182             ChargeType chargeType, DatahubTariffFilter tariffFilter, Map<String, String> properties)
183             throws InterruptedException, DataServiceException {
184         String columns = "ValidFrom,ValidTo,ChargeTypeCode";
185         for (int i = 1; i < 25; i++) {
186             columns += ",Price" + i;
187         }
188
189         Map<String, Collection<String>> filterMap = new HashMap<>(Map.of( //
190                 FILTER_KEY_GLN_NUMBER, List.of(globalLocationNumber.toString()), //
191                 FILTER_KEY_CHARGE_TYPE, List.of(chargeType.toString())));
192
193         Collection<String> chargeTypeCodes = tariffFilter.getChargeTypeCodesAsStrings();
194         if (!chargeTypeCodes.isEmpty()) {
195             filterMap.put(FILTER_KEY_CHARGE_TYPE_CODE, chargeTypeCodes);
196         }
197
198         Collection<String> notes = tariffFilter.getNotes();
199         if (!notes.isEmpty()) {
200             filterMap.put(FILTER_KEY_NOTE, notes);
201         }
202
203         Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST)
204                 .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) //
205                 .param("filter", mapToFilter(filterMap)) //
206                 .param("columns", columns) //
207                 .agent(userAgent) //
208                 .method(HttpMethod.GET);
209
210         DateQueryParameter dateQueryParameter = tariffFilter.getDateQueryParameter();
211         if (!dateQueryParameter.isEmpty()) {
212             request = request.param("start", dateQueryParameter.toString());
213         }
214
215         logger.trace("GET request for {}", request.getURI());
216
217         try {
218             ContentResponse response = request.send();
219
220             updatePropertiesFromResponse(response, properties);
221
222             int status = response.getStatus();
223             if (!HttpStatus.isSuccess(status)) {
224                 throw new DataServiceException("The request failed with HTTP error " + status, status);
225             }
226             String responseContent = response.getContentAsString();
227             if (responseContent.isEmpty()) {
228                 throw new DataServiceException("Empty response");
229             }
230             logger.trace("Response content: '{}'", responseContent);
231
232             DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class);
233             if (records == null) {
234                 throw new DataServiceException("Error parsing response");
235             }
236
237             if (records.limit() > 0 && records.limit() < records.total()) {
238                 logger.warn("{} price list records available, but only {} returned.", records.total(), records.limit());
239             }
240
241             if (Objects.isNull(records.records())) {
242                 return List.of();
243             }
244
245             return Arrays.stream(records.records()).filter(Objects::nonNull).toList();
246         } catch (JsonSyntaxException e) {
247             throw new DataServiceException("Error parsing response", e);
248         } catch (TimeoutException | ExecutionException e) {
249             throw new DataServiceException(e);
250         }
251     }
252
253     private String mapToFilter(Map<String, Collection<String>> map) {
254         return "{" + map.entrySet().stream().map(
255                 e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]")
256                 .collect(Collectors.joining(",")) + "}";
257     }
258 }