]> git.basschouten.com Git - openhab-addons.git/blob
aba5973ef2561a1ba03a84ffde8d926eb7bfa779
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
54
55     private Map<DatahubTariff, Collection<DatahubPricelistRecord>> datahubRecordsMap = new HashMap<>();
56     private Map<DatahubTariff, Map<Instant, BigDecimal>> tariffsMap = new ConcurrentHashMap<>();
57
58     public CacheManager() {
59         this(Clock.systemDefaultZone());
60     }
61
62     public CacheManager(Clock clock) {
63         this.clock = clock.withZone(NORD_POOL_TIMEZONE);
64
65         for (DatahubTariff datahubTariff : DatahubTariff.values()) {
66             datahubRecordsMap.put(datahubTariff, new ArrayList<>());
67             tariffsMap.put(datahubTariff, new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE));
68         }
69     }
70
71     /**
72      * Clear all cached data.
73      */
74     public void clear() {
75         spotPriceMap.clear();
76
77         for (DatahubTariff datahubTariff : DatahubTariff.values()) {
78             Collection<DatahubPricelistRecord> datahubRecords = datahubRecordsMap.get(datahubTariff);
79             if (datahubRecords != null) {
80                 datahubRecords.clear();
81             }
82
83             Map<Instant, BigDecimal> tariffs = tariffsMap.get(datahubTariff);
84             if (tariffs != null) {
85                 tariffs.clear();
86             }
87         }
88     }
89
90     /**
91      * Convert and cache the supplied {@link ElspotpriceRecord}s.
92      * 
93      * @param records The records as received from Energi Data Service.
94      * @param currency The currency in which the records were requested.
95      */
96     public void putSpotPrices(ElspotpriceRecord[] records, Currency currency) {
97         boolean isDKK = EnergiDataServiceBindingConstants.CURRENCY_DKK.equals(currency);
98         for (ElspotpriceRecord record : records) {
99             spotPriceMap.put(record.hour(),
100                     (isDKK ? record.spotPriceDKK() : record.spotPriceEUR()).divide(BigDecimal.valueOf(1000)));
101         }
102         cleanup();
103     }
104
105     /**
106      * Replace current "raw"/unprocessed tariff records in cache.
107      * Map of hourly tariffs will be updated automatically.
108      *
109      * @param records to cache
110      */
111     public void putTariffs(DatahubTariff datahubTariff, Collection<DatahubPricelistRecord> records) {
112         Collection<DatahubPricelistRecord> datahubRecords = datahubRecordsMap.get(datahubTariff);
113         if (datahubRecords == null) {
114             throw new IllegalStateException("Datahub records not initialized");
115         }
116         putDatahubRecords(datahubRecords, records);
117         updateTariffs(datahubTariff);
118     }
119
120     private void putDatahubRecords(Collection<DatahubPricelistRecord> destination,
121             Collection<DatahubPricelistRecord> source) {
122         LocalDateTime localHourStart = LocalDateTime.now(clock.withZone(DATAHUB_TIMEZONE))
123                 .minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
124
125         destination.clear();
126         destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
127     }
128
129     /**
130      * Update map of hourly tariffs from internal cache.
131      */
132     public void updateTariffs(DatahubTariff datahubTariff) {
133         Collection<DatahubPricelistRecord> datahubRecords = datahubRecordsMap.get(datahubTariff);
134         if (datahubRecords == null) {
135             throw new IllegalStateException("Datahub records not initialized");
136         }
137         tariffsMap.put(datahubTariff, priceListParser.toHourly(datahubRecords));
138         cleanup();
139     }
140
141     /**
142      * Get current spot price.
143      *
144      * @return spot price currently valid
145      */
146     public @Nullable BigDecimal getSpotPrice() {
147         return getSpotPrice(Instant.now(clock));
148     }
149
150     /**
151      * Get spot price valid at provided instant.
152      *
153      * @param time {@link Instant} for which to get the spot price
154      * @return spot price at given time or null if not available
155      */
156     public @Nullable BigDecimal getSpotPrice(Instant time) {
157         return spotPriceMap.get(getHourStart(time));
158     }
159
160     /**
161      * Get map of all cached spot prices.
162      *
163      * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
164      */
165     public Map<Instant, BigDecimal> getSpotPrices() {
166         return new HashMap<>(spotPriceMap);
167     }
168
169     /**
170      * Get current tariff.
171      *
172      * @return tariff currently valid
173      */
174     public @Nullable BigDecimal getTariff(DatahubTariff datahubTariff) {
175         return getTariff(datahubTariff, Instant.now(clock));
176     }
177
178     /**
179      * Get tariff valid at provided instant.
180      *
181      * @param time {@link Instant} for which to get the tariff
182      * @return tariff at given time or null if not available
183      */
184     public @Nullable BigDecimal getTariff(DatahubTariff datahubTariff, Instant time) {
185         Map<Instant, BigDecimal> tariffs = tariffsMap.get(datahubTariff);
186         if (tariffs == null) {
187             throw new IllegalStateException("Tariffs not initialized");
188         }
189         return tariffs.get(getHourStart(time));
190     }
191
192     /**
193      * Get map of all cached tariffs.
194      *
195      * @return tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
196      */
197     public Map<Instant, BigDecimal> getTariffs(DatahubTariff datahubTariff) {
198         Map<Instant, BigDecimal> tariffs = tariffsMap.get(datahubTariff);
199         if (tariffs == null) {
200             throw new IllegalStateException("Tariffs not initialized");
201         }
202         return new HashMap<>(tariffs);
203     }
204
205     /**
206      * Get number of future spot prices including current hour.
207      * 
208      * @return number of future spot prices
209      */
210     public long getNumberOfFutureSpotPrices() {
211         Instant currentHourStart = getCurrentHourStart();
212
213         return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
214     }
215
216     /**
217      * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
218      * 
219      * @return true if historic spot prices are cached
220      */
221     public boolean areHistoricSpotPricesCached() {
222         return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
223     }
224
225     /**
226      * Check if all current spot prices are cached taking into consideration that next day's spot prices
227      * should be available at 13:00 CET.
228      *
229      * @return true if spot prices are fully cached
230      */
231     public boolean areSpotPricesFullyCached() {
232         Instant end = ZonedDateTime.of(LocalDate.now(clock), LocalTime.of(23, 0), NORD_POOL_TIMEZONE).toInstant();
233         LocalTime now = LocalTime.now(clock);
234         if (now.isAfter(DAILY_REFRESH_TIME_CET)) {
235             end = end.plus(24, ChronoUnit.HOURS);
236         }
237
238         return arePricesCached(spotPriceMap, end);
239     }
240
241     private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
242         for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
243                 ChronoUnit.HOURS)) {
244             if (priceMap.get(hourStart) == null) {
245                 return false;
246             }
247         }
248
249         return true;
250     }
251
252     /**
253      * Check if we have "raw" tariff records cached which are valid tomorrow.
254      * 
255      * @return true if tariff records for tomorrow are cached
256      */
257     public boolean areTariffsValidTomorrow(DatahubTariff datahubTariff) {
258         Collection<DatahubPricelistRecord> datahubRecords = datahubRecordsMap.get(datahubTariff);
259         if (datahubRecords == null) {
260             throw new IllegalStateException("Datahub records not initialized");
261         }
262         return isValidNextDay(datahubRecords);
263     }
264
265     /**
266      * Remove historic prices.
267      */
268     public void cleanup() {
269         Instant firstHourStart = getFirstHourStart();
270
271         spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
272
273         for (Map<Instant, BigDecimal> tariffs : tariffsMap.values()) {
274             tariffs.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
275         }
276     }
277
278     private boolean isValidNextDay(Collection<DatahubPricelistRecord> records) {
279         LocalDateTime localHourStart = LocalDateTime.now(EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE)
280                 .truncatedTo(ChronoUnit.HOURS);
281         LocalDateTime localMidnight = localHourStart.plusDays(1).truncatedTo(ChronoUnit.DAYS);
282
283         return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
284     }
285
286     private Instant getCurrentHourStart() {
287         return getHourStart(Instant.now(clock));
288     }
289
290     private Instant getFirstHourStart() {
291         return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
292     }
293
294     private Instant getHourStart(Instant instant) {
295         return instant.truncatedTo(ChronoUnit.HOURS);
296     }
297 }