]> git.basschouten.com Git - openhab-addons.git/blob
d73ee12dd1d910b0e3106e0c8db30200c1e5b7b4
[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.math.BigDecimal;
18 import java.time.Clock;
19 import java.time.Instant;
20 import java.time.LocalDate;
21 import java.time.LocalDateTime;
22 import java.time.LocalTime;
23 import java.time.ZonedDateTime;
24 import java.time.temporal.ChronoUnit;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Currency;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.concurrent.ConcurrentHashMap;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
35 import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
36
37 /**
38  * The {@link CacheManager} is responsible for maintaining a cache of received
39  * data from Energi Data Service.
40  *
41  * @author Jacob Laursen - Initial contribution
42  */
43 @NonNullByDefault
44 public class CacheManager {
45
46     public static final int NUMBER_OF_HISTORIC_HOURS = 24;
47     public static final int SPOT_PRICE_MAX_CACHE_SIZE = 24 + 11 + NUMBER_OF_HISTORIC_HOURS;
48     public static final int TARIFF_MAX_CACHE_SIZE = 24 * 2 + NUMBER_OF_HISTORIC_HOURS;
49
50     private final Clock clock;
51     private final PriceListParser priceListParser = new PriceListParser();
52
53     private Collection<DatahubPricelistRecord> netTariffRecords = new ArrayList<>();
54     private Collection<DatahubPricelistRecord> systemTariffRecords = new ArrayList<>();
55     private Collection<DatahubPricelistRecord> electricityTaxRecords = new ArrayList<>();
56     private Collection<DatahubPricelistRecord> transmissionNetTariffRecords = new ArrayList<>();
57
58     private Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
59     private Map<Instant, BigDecimal> netTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
60     private Map<Instant, BigDecimal> systemTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
61     private Map<Instant, BigDecimal> electricityTaxMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
62     private Map<Instant, BigDecimal> transmissionNetTariffMap = new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE);
63
64     public CacheManager() {
65         this(Clock.systemDefaultZone());
66     }
67
68     public CacheManager(Clock clock) {
69         this.clock = clock.withZone(NORD_POOL_TIMEZONE);
70     }
71
72     /**
73      * Clear all cached data.
74      */
75     public void clear() {
76         netTariffRecords.clear();
77         systemTariffRecords.clear();
78         electricityTaxRecords.clear();
79         transmissionNetTariffRecords.clear();
80
81         spotPriceMap.clear();
82         netTariffMap.clear();
83         systemTariffMap.clear();
84         electricityTaxMap.clear();
85         transmissionNetTariffMap.clear();
86     }
87
88     /**
89      * Convert and cache the supplied {@link ElspotpriceRecord}s.
90      * 
91      * @param records The records as received from Energi Data Service.
92      * @param currency The currency in which the records were requested.
93      */
94     public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) {
95         boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency);
96         for (ElspotpriceRecord record : records) {
97             spotPriceMap.put(record.hour(),
98                     (isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000)));
99         }
100         cleanup();
101     }
102
103     /**
104      * Replace current "raw"/unprocessed net tariff records in cache.
105      * Map of hourly tariffs will be updated automatically.
106      *
107      * @param records to cache
108      */
109     public void putNetTariffs(Collection<DatahubPricelistRecord> records) {
110         putDatahubRecords(netTariffRecords, records);
111         updateNetTariffs();
112     }
113
114     /**
115      * Replace current "raw"/unprocessed system tariff records in cache.
116      * Map of hourly tariffs will be updated automatically.
117      *
118      * @param records to cache
119      */
120     public void putSystemTariffs(Collection<DatahubPricelistRecord> records) {
121         putDatahubRecords(systemTariffRecords, records);
122         updateSystemTariffs();
123     }
124
125     /**
126      * Replace current "raw"/unprocessed electricity tax records in cache.
127      * Map of hourly taxes will be updated automatically.
128      *
129      * @param records to cache
130      */
131     public void putElectricityTaxes(Collection<DatahubPricelistRecord> records) {
132         putDatahubRecords(electricityTaxRecords, records);
133         updateElectricityTaxes();
134     }
135
136     /**
137      * Replace current "raw"/unprocessed transmission net tariff records in cache.
138      * Map of hourly tariffs will be updated automatically.
139      *
140      * @param records to cache
141      */
142     public void putTransmissionNetTariffs(Collection<DatahubPricelistRecord> records) {
143         putDatahubRecords(transmissionNetTariffRecords, records);
144         updateTransmissionNetTariffs();
145     }
146
147     private void putDatahubRecords(Collection<DatahubPricelistRecord> destination,
148             Collection<DatahubPricelistRecord> source) {
149         LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE))
150                 .minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
151
152         destination.clear();
153         destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
154     }
155
156     /**
157      * Update map of hourly net tariffs from internal cache.
158      */
159     public void updateNetTariffs() {
160         netTariffMap = priceListParser.toHourly(netTariffRecords);
161         cleanup();
162     }
163
164     /**
165      * Update map of system tariffs from internal cache.
166      */
167     public void updateSystemTariffs() {
168         systemTariffMap = priceListParser.toHourly(systemTariffRecords);
169         cleanup();
170     }
171
172     /**
173      * Update map of electricity taxes from internal cache.
174      */
175     public void updateElectricityTaxes() {
176         electricityTaxMap = priceListParser.toHourly(electricityTaxRecords);
177         cleanup();
178     }
179
180     /**
181      * Update map of hourly transmission net tariffs from internal cache.
182      */
183     public void updateTransmissionNetTariffs() {
184         transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords);
185         cleanup();
186     }
187
188     /**
189      * Get current spot price.
190      *
191      * @return spot price currently valid
192      */
193     public @Nullable BigDecimal getSpotPrice() {
194         return getSpotPrice(Instant.now(clock));
195     }
196
197     /**
198      * Get spot price valid at provided instant.
199      *
200      * @param time {@link Instant} for which to get the spot price
201      * @return spot price at given time or null if not available
202      */
203     public @Nullable BigDecimal getSpotPrice(Instant time) {
204         return spotPriceMap.get(getHourStart(time));
205     }
206
207     /**
208      * Get map of all cached spot prices.
209      *
210      * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
211      */
212     public Map<Instant, BigDecimal> getSpotPrices() {
213         return new HashMap<Instant, BigDecimal>(spotPriceMap);
214     }
215
216     /**
217      * Get current net tariff.
218      *
219      * @return net tariff currently valid
220      */
221     public @Nullable BigDecimal getNetTariff() {
222         return getNetTariff(Instant.now(clock));
223     }
224
225     /**
226      * Get net tariff valid at provided instant.
227      *
228      * @param time {@link Instant} for which to get the net tariff
229      * @return net tariff at given time or null if not available
230      */
231     public @Nullable BigDecimal getNetTariff(Instant time) {
232         return netTariffMap.get(getHourStart(time));
233     }
234
235     /**
236      * Get map of all cached net tariffs.
237      *
238      * @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
239      */
240     public Map<Instant, BigDecimal> getNetTariffs() {
241         return new HashMap<Instant, BigDecimal>(netTariffMap);
242     }
243
244     /**
245      * Get current system tariff.
246      *
247      * @return system tariff currently valid
248      */
249     public @Nullable BigDecimal getSystemTariff() {
250         return getSystemTariff(Instant.now(clock));
251     }
252
253     /**
254      * Get system tariff valid at provided instant.
255      *
256      * @param time {@link Instant} for which to get the system tariff
257      * @return system tariff at given time or null if not available
258      */
259     public @Nullable BigDecimal getSystemTariff(Instant time) {
260         return systemTariffMap.get(getHourStart(time));
261     }
262
263     /**
264      * Get map of all cached system tariffs.
265      *
266      * @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
267      */
268     public Map<Instant, BigDecimal> getSystemTariffs() {
269         return new HashMap<Instant, BigDecimal>(systemTariffMap);
270     }
271
272     /**
273      * Get current electricity tax.
274      *
275      * @return electricity tax currently valid
276      */
277     public @Nullable BigDecimal getElectricityTax() {
278         return getElectricityTax(Instant.now(clock));
279     }
280
281     /**
282      * Get electricity tax valid at provided instant.
283      *
284      * @param time {@link Instant} for which to get the electricity tax
285      * @return electricity tax at given time or null if not available
286      */
287     public @Nullable BigDecimal getElectricityTax(Instant time) {
288         return electricityTaxMap.get(getHourStart(time));
289     }
290
291     /**
292      * Get map of all cached electricity taxes.
293      *
294      * @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
295      */
296     public Map<Instant, BigDecimal> getElectricityTaxes() {
297         return new HashMap<Instant, BigDecimal>(electricityTaxMap);
298     }
299
300     /**
301      * Get current transmission net tariff.
302      *
303      * @return transmission net tariff currently valid
304      */
305     public @Nullable BigDecimal getTransmissionNetTariff() {
306         return getTransmissionNetTariff(Instant.now(clock));
307     }
308
309     /**
310      * Get transmission net tariff valid at provided instant.
311      *
312      * @param time {@link Instant} for which to get the transmission net tariff
313      * @return transmission net tariff at given time or null if not available
314      */
315     public @Nullable BigDecimal getTransmissionNetTariff(Instant time) {
316         return transmissionNetTariffMap.get(getHourStart(time));
317     }
318
319     /**
320      * Get map of all cached transmission net tariffs.
321      *
322      * @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
323      */
324     public Map<Instant, BigDecimal> getTransmissionNetTariffs() {
325         return new HashMap<Instant, BigDecimal>(transmissionNetTariffMap);
326     }
327
328     /**
329      * Get number of future spot prices including current hour.
330      * 
331      * @return number of future spot prices
332      */
333     public long getNumberOfFutureSpotPrices() {
334         Instant currentHourStart = getCurrentHourStart();
335
336         return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
337     }
338
339     /**
340      * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
341      * 
342      * @return true if historic spot prices are cached
343      */
344     public boolean areHistoricSpotPricesCached() {
345         return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
346     }
347
348     /**
349      * Check if all current spot prices are cached taking into consideration that next day's spot prices
350      * should be available at 13:00 CET.
351      *
352      * @return true if spot prices are fully cached
353      */
354     public boolean areSpotPricesFullyCached() {
355         Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant();
356         LocalTime now = LocalTime.now(clock);
357         if (now.isAfter(DAILY_REFRESH_TIME_CET)) {
358             end = end.plus(24, ChronoUnit.HOURS);
359         }
360
361         return arePricesCached(spotPriceMap, end);
362     }
363
364     private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
365         for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
366                 ChronoUnit.HOURS)) {
367             if (priceMap.get(hourStart) == null) {
368                 return false;
369             }
370         }
371
372         return true;
373     }
374
375     /**
376      * Check if we have "raw" net tariff records cached which are valid tomorrow.
377      * 
378      * @return true if net tariff records for tomorrow are cached
379      */
380     public boolean areNetTariffsValidTomorrow() {
381         return isValidNextDay(netTariffRecords);
382     }
383
384     /**
385      * Check if we have "raw" system tariff records cached which are valid tomorrow.
386      * 
387      * @return true if system tariff records for tomorrow are cached
388      */
389     public boolean areSystemTariffsValidTomorrow() {
390         return isValidNextDay(systemTariffRecords);
391     }
392
393     /**
394      * Check if we have "raw" electricity tax records cached which are valid tomorrow.
395      * 
396      * @return true if electricity tax records for tomorrow are cached
397      */
398     public boolean areElectricityTaxesValidTomorrow() {
399         return isValidNextDay(electricityTaxRecords);
400     }
401
402     /**
403      * Check if we have "raw" transmission net tariff records cached which are valid tomorrow.
404      * 
405      * @return true if transmission net tariff records for tomorrow are cached
406      */
407     public boolean areTransmissionNetTariffsValidTomorrow() {
408         return isValidNextDay(transmissionNetTariffRecords);
409     }
410
411     /**
412      * Remove historic prices.
413      */
414     public void cleanup() {
415         Instant firstHourStart = getFirstHourStart();
416
417         spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
418         netTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
419         systemTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
420         electricityTaxMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
421         transmissionNetTariffMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
422     }
423
424     private boolean isValidNextDay(Collection<DatahubPricelistRecord> records) {
425         LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
426                 .truncatedTo(ChronoUnit.HOURS);
427         LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS);
428
429         return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
430     }
431
432     private Instant getCurrentHourStart() {
433         return getHourStart(Instant.now(clock));
434     }
435
436     private Instant getFirstHourStart() {
437         return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
438     }
439
440     private Instant getHourStart(Instant instant) {
441         return instant.truncatedTo(ChronoUnit.HOURS);
442     }
443 }