2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.energidataservice.internal;
15 import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
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;
30 import java.util.concurrent.ConcurrentHashMap;
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;
38 * The {@link CacheManager} is responsible for maintaining a cache of received
39 * data from Energi Data Service.
41 * @author Jacob Laursen - Initial contribution
44 public class CacheManager {
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;
50 private final Clock clock;
51 private final PriceListParser priceListParser = new PriceListParser();
53 private Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
55 private Map<DatahubTariff, Collection<DatahubPricelistRecord>> datahubRecordsMap = new HashMap<>();
56 private Map<DatahubTariff, Map<Instant, BigDecimal>> tariffsMap = new ConcurrentHashMap<>();
58 public CacheManager() {
59 this(Clock.systemDefaultZone());
62 public CacheManager(Clock clock) {
63 this.clock = clock.withZone(NORD_POOL_TIMEZONE);
65 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
66 datahubRecordsMap.put(datahubTariff, new ArrayList<>());
67 tariffsMap.put(datahubTariff, new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE));
72 * Clear all cached data.
77 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
78 Collection<DatahubPricelistRecord> datahubRecords = datahubRecordsMap.get(datahubTariff);
79 if (datahubRecords != null) {
80 datahubRecords.clear();
83 Map<Instant, BigDecimal> tariffs = tariffsMap.get(datahubTariff);
84 if (tariffs != null) {
91 * Convert and cache the supplied {@link ElspotpriceRecord}s.
93 * @param records The records as received from Energi Data Service.
94 * @param currency The currency in which the records were requested.
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)));
106 * Replace current "raw"/unprocessed tariff records in cache.
107 * Map of hourly tariffs will be updated automatically.
109 * @param records to cache
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");
116 putDatahubRecords(datahubRecords, records);
117 updateTariffs(datahubTariff);
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);
126 destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
130 * Update map of hourly tariffs from internal cache.
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");
137 tariffsMap.put(datahubTariff, priceListParser.toHourly(datahubRecords));
142 * Get current spot price.
144 * @return spot price currently valid
146 public @Nullable BigDecimal getSpotPrice() {
147 return getSpotPrice(Instant.now(clock));
151 * Get spot price valid at provided instant.
153 * @param time {@link Instant} for which to get the spot price
154 * @return spot price at given time or null if not available
156 public @Nullable BigDecimal getSpotPrice(Instant time) {
157 return spotPriceMap.get(getHourStart(time));
161 * Get map of all cached spot prices.
163 * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
165 public Map<Instant, BigDecimal> getSpotPrices() {
166 return new HashMap<>(spotPriceMap);
170 * Get current tariff.
172 * @return tariff currently valid
174 public @Nullable BigDecimal getTariff(DatahubTariff datahubTariff) {
175 return getTariff(datahubTariff, Instant.now(clock));
179 * Get tariff valid at provided instant.
181 * @param time {@link Instant} for which to get the tariff
182 * @return tariff at given time or null if not available
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");
189 return tariffs.get(getHourStart(time));
193 * Get map of all cached tariffs.
195 * @return tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
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");
202 return new HashMap<>(tariffs);
206 * Get number of future spot prices including current hour.
208 * @return number of future spot prices
210 public long getNumberOfFutureSpotPrices() {
211 Instant currentHourStart = getCurrentHourStart();
213 return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
217 * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
219 * @return true if historic spot prices are cached
221 public boolean areHistoricSpotPricesCached() {
222 return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
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.
229 * @return true if spot prices are fully cached
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);
238 return arePricesCached(spotPriceMap, end);
241 private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
242 for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
244 if (priceMap.get(hourStart) == null) {
253 * Check if we have "raw" tariff records cached which are valid tomorrow.
255 * @return true if tariff records for tomorrow are cached
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");
262 return isValidNextDay(datahubRecords);
266 * Remove historic prices.
268 public void cleanup() {
269 Instant firstHourStart = getFirstHourStart();
271 spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
273 for (Map<Instant, BigDecimal> tariffs : tariffsMap.values()) {
274 tariffs.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
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);
283 return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
286 private Instant getCurrentHourStart() {
287 return getHourStart(Instant.now(clock));
290 private Instant getFirstHourStart() {
291 return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
294 private Instant getHourStart(Instant instant) {
295 return instant.truncatedTo(ChronoUnit.HOURS);