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