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