2 * Copyright (c) 2010-2023 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<DatahubTariff, Collection<DatahubPricelistRecord>> datahubRecordsMap = new HashMap<>();
55 private Map<Instant, BigDecimal> spotPriceMap = new ConcurrentHashMap<>(SPOT_PRICE_MAX_CACHE_SIZE);
57 private Map<DatahubTariff, Map<Instant, BigDecimal>> tariffsMap = new ConcurrentHashMap<>();
59 public CacheManager() {
60 this(Clock.systemDefaultZone());
63 public CacheManager(Clock clock) {
64 this.clock = clock.withZone(NORD_POOL_TIMEZONE);
66 for (DatahubTariff tariff : DatahubTariff.values()) {
67 datahubRecordsMap.put(tariff, new ArrayList<>());
68 tariffsMap.put(tariff, new ConcurrentHashMap<>(TARIFF_MAX_CACHE_SIZE));
73 * Clear all cached data.
76 datahubRecordsMap.clear();
82 * Convert and cache the supplied {@link ElspotpriceRecord}s.
84 * @param records The records as received from Energi Data Service.
85 * @param currency The currency in which the records were requested.
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)));
97 * Replace current "raw"/unprocessed tariff records in cache.
98 * Map of hourly tariffs will be updated automatically.
100 * @param records to cache
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");
107 putDatahubRecords(datahubRecords, records);
108 updateTariffs(datahubTariff);
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);
117 destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
121 * Update map of hourly tariffs from internal cache.
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");
128 tariffsMap.put(datahubTariff, priceListParser.toHourly(datahubRecords));
133 * Get current spot price.
135 * @return spot price currently valid
137 public @Nullable BigDecimal getSpotPrice() {
138 return getSpotPrice(Instant.now(clock));
142 * Get spot price valid at provided instant.
144 * @param time {@link Instant} for which to get the spot price
145 * @return spot price at given time or null if not available
147 public @Nullable BigDecimal getSpotPrice(Instant time) {
148 return spotPriceMap.get(getHourStart(time));
152 * Get map of all cached spot prices.
154 * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
156 public Map<Instant, BigDecimal> getSpotPrices() {
157 return new HashMap<Instant, BigDecimal>(spotPriceMap);
161 * Get current tariff.
163 * @return tariff currently valid
165 public @Nullable BigDecimal getTariff(DatahubTariff datahubTariff) {
166 return getTariff(datahubTariff, Instant.now(clock));
170 * Get tariff valid at provided instant.
172 * @param time {@link Instant} for which to get the tariff
173 * @return tariff at given time or null if not available
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");
180 return tariffs.get(getHourStart(time));
184 * Get map of all cached tariffs.
186 * @return tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
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");
193 return new HashMap<Instant, BigDecimal>(tariffs);
197 * Get number of future spot prices including current hour.
199 * @return number of future spot prices
201 public long getNumberOfFutureSpotPrices() {
202 Instant currentHourStart = getCurrentHourStart();
204 return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
208 * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
210 * @return true if historic spot prices are cached
212 public boolean areHistoricSpotPricesCached() {
213 return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
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.
220 * @return true if spot prices are fully cached
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);
229 return arePricesCached(spotPriceMap, end);
232 private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
233 for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
235 if (priceMap.get(hourStart) == null) {
244 * Check if we have "raw" tariff records cached which are valid tomorrow.
246 * @return true if tariff records for tomorrow are cached
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");
253 return isValidNextDay(datahubRecords);
257 * Remove historic prices.
259 public void cleanup() {
260 Instant firstHourStart = getFirstHourStart();
262 spotPriceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
264 for (Map<Instant, BigDecimal> tariffs : tariffsMap.values()) {
265 tariffs.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
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);
274 return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
277 private Instant getCurrentHourStart() {
278 return getHourStart(Instant.now(clock));
281 private Instant getFirstHourStart() {
282 return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
285 private Instant getHourStart(Instant instant) {
286 return instant.truncatedTo(ChronoUnit.HOURS);