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 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<>();
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);
64 public CacheManager() {
65 this(Clock.systemDefaultZone());
68 public CacheManager(Clock clock) {
69 this.clock = clock.withZone(NORD_POOL_TIMEZONE);
73 * Clear all cached data.
76 netTariffRecords.clear();
77 systemTariffRecords.clear();
78 electricityTaxRecords.clear();
79 transmissionNetTariffRecords.clear();
83 systemTariffMap.clear();
84 electricityTaxMap.clear();
85 transmissionNetTariffMap.clear();
89 * Convert and cache the supplied {@link ElspotpriceRecord}s.
91 * @param records The records as received from Energi Data Service.
92 * @param currency The currency in which the records were requested.
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)));
104 * Replace current "raw"/unprocessed net tariff records in cache.
105 * Map of hourly tariffs will be updated automatically.
107 * @param records to cache
109 public void putNetTariffs(Collection<DatahubPricelistRecord> records) {
110 putDatahubRecords(netTariffRecords, records);
115 * Replace current "raw"/unprocessed system tariff records in cache.
116 * Map of hourly tariffs will be updated automatically.
118 * @param records to cache
120 public void putSystemTariffs(Collection<DatahubPricelistRecord> records) {
121 putDatahubRecords(systemTariffRecords, records);
122 updateSystemTariffs();
126 * Replace current "raw"/unprocessed electricity tax records in cache.
127 * Map of hourly taxes will be updated automatically.
129 * @param records to cache
131 public void putElectricityTaxes(Collection<DatahubPricelistRecord> records) {
132 putDatahubRecords(electricityTaxRecords, records);
133 updateElectricityTaxes();
137 * Replace current "raw"/unprocessed transmission net tariff records in cache.
138 * Map of hourly tariffs will be updated automatically.
140 * @param records to cache
142 public void putTransmissionNetTariffs(Collection<DatahubPricelistRecord> records) {
143 putDatahubRecords(transmissionNetTariffRecords, records);
144 updateTransmissionNetTariffs();
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);
153 destination.addAll(source.stream().filter(r -> !r.validTo().isBefore(localHourStart)).toList());
157 * Update map of hourly net tariffs from internal cache.
159 public void updateNetTariffs() {
160 netTariffMap = priceListParser.toHourly(netTariffRecords);
165 * Update map of system tariffs from internal cache.
167 public void updateSystemTariffs() {
168 systemTariffMap = priceListParser.toHourly(systemTariffRecords);
173 * Update map of electricity taxes from internal cache.
175 public void updateElectricityTaxes() {
176 electricityTaxMap = priceListParser.toHourly(electricityTaxRecords);
181 * Update map of hourly transmission net tariffs from internal cache.
183 public void updateTransmissionNetTariffs() {
184 transmissionNetTariffMap = priceListParser.toHourly(transmissionNetTariffRecords);
189 * Get current spot price.
191 * @return spot price currently valid
193 public @Nullable BigDecimal getSpotPrice() {
194 return getSpotPrice(Instant.now(clock));
198 * Get spot price valid at provided instant.
200 * @param time {@link Instant} for which to get the spot price
201 * @return spot price at given time or null if not available
203 public @Nullable BigDecimal getSpotPrice(Instant time) {
204 return spotPriceMap.get(getHourStart(time));
208 * Get map of all cached spot prices.
210 * @return spot prices currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
212 public Map<Instant, BigDecimal> getSpotPrices() {
213 return new HashMap<Instant, BigDecimal>(spotPriceMap);
217 * Get current net tariff.
219 * @return net tariff currently valid
221 public @Nullable BigDecimal getNetTariff() {
222 return getNetTariff(Instant.now(clock));
226 * Get net tariff valid at provided instant.
228 * @param time {@link Instant} for which to get the net tariff
229 * @return net tariff at given time or null if not available
231 public @Nullable BigDecimal getNetTariff(Instant time) {
232 return netTariffMap.get(getHourStart(time));
236 * Get map of all cached net tariffs.
238 * @return net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
240 public Map<Instant, BigDecimal> getNetTariffs() {
241 return new HashMap<Instant, BigDecimal>(netTariffMap);
245 * Get current system tariff.
247 * @return system tariff currently valid
249 public @Nullable BigDecimal getSystemTariff() {
250 return getSystemTariff(Instant.now(clock));
254 * Get system tariff valid at provided instant.
256 * @param time {@link Instant} for which to get the system tariff
257 * @return system tariff at given time or null if not available
259 public @Nullable BigDecimal getSystemTariff(Instant time) {
260 return systemTariffMap.get(getHourStart(time));
264 * Get map of all cached system tariffs.
266 * @return system tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
268 public Map<Instant, BigDecimal> getSystemTariffs() {
269 return new HashMap<Instant, BigDecimal>(systemTariffMap);
273 * Get current electricity tax.
275 * @return electricity tax currently valid
277 public @Nullable BigDecimal getElectricityTax() {
278 return getElectricityTax(Instant.now(clock));
282 * Get electricity tax valid at provided instant.
284 * @param time {@link Instant} for which to get the electricity tax
285 * @return electricity tax at given time or null if not available
287 public @Nullable BigDecimal getElectricityTax(Instant time) {
288 return electricityTaxMap.get(getHourStart(time));
292 * Get map of all cached electricity taxes.
294 * @return electricity taxes currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
296 public Map<Instant, BigDecimal> getElectricityTaxes() {
297 return new HashMap<Instant, BigDecimal>(electricityTaxMap);
301 * Get current transmission net tariff.
303 * @return transmission net tariff currently valid
305 public @Nullable BigDecimal getTransmissionNetTariff() {
306 return getTransmissionNetTariff(Instant.now(clock));
310 * Get transmission net tariff valid at provided instant.
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
315 public @Nullable BigDecimal getTransmissionNetTariff(Instant time) {
316 return transmissionNetTariffMap.get(getHourStart(time));
320 * Get map of all cached transmission net tariffs.
322 * @return transmission net tariffs currently available, {@link #NUMBER_OF_HISTORIC_HOURS} back
324 public Map<Instant, BigDecimal> getTransmissionNetTariffs() {
325 return new HashMap<Instant, BigDecimal>(transmissionNetTariffMap);
329 * Get number of future spot prices including current hour.
331 * @return number of future spot prices
333 public long getNumberOfFutureSpotPrices() {
334 Instant currentHourStart = getCurrentHourStart();
336 return spotPriceMap.entrySet().stream().filter(p -> !p.getKey().isBefore(currentHourStart)).count();
340 * Check if historic spot prices ({@link #NUMBER_OF_HISTORIC_HOURS}) are cached.
342 * @return true if historic spot prices are cached
344 public boolean areHistoricSpotPricesCached() {
345 return arePricesCached(spotPriceMap, getCurrentHourStart().minus(1, ChronoUnit.HOURS));
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.
352 * @return true if spot prices are fully cached
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);
361 return arePricesCached(spotPriceMap, end);
364 private boolean arePricesCached(Map<Instant, BigDecimal> priceMap, Instant end) {
365 for (Instant hourStart = getFirstHourStart(); hourStart.compareTo(end) <= 0; hourStart = hourStart.plus(1,
367 if (priceMap.get(hourStart) == null) {
376 * Check if we have "raw" net tariff records cached which are valid tomorrow.
378 * @return true if net tariff records for tomorrow are cached
380 public boolean areNetTariffsValidTomorrow() {
381 return isValidNextDay(netTariffRecords);
385 * Check if we have "raw" system tariff records cached which are valid tomorrow.
387 * @return true if system tariff records for tomorrow are cached
389 public boolean areSystemTariffsValidTomorrow() {
390 return isValidNextDay(systemTariffRecords);
394 * Check if we have "raw" electricity tax records cached which are valid tomorrow.
396 * @return true if electricity tax records for tomorrow are cached
398 public boolean areElectricityTaxesValidTomorrow() {
399 return isValidNextDay(electricityTaxRecords);
403 * Check if we have "raw" transmission net tariff records cached which are valid tomorrow.
405 * @return true if transmission net tariff records for tomorrow are cached
407 public boolean areTransmissionNetTariffsValidTomorrow() {
408 return isValidNextDay(transmissionNetTariffRecords);
412 * Remove historic prices.
414 public void cleanup() {
415 Instant firstHourStart = getFirstHourStart();
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));
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);
429 return records.stream().anyMatch(r -> r.validTo().isAfter(localMidnight));
432 private Instant getCurrentHourStart() {
433 return getHourStart(Instant.now(clock));
436 private Instant getFirstHourStart() {
437 return getHourStart(Instant.now(clock).minus(NUMBER_OF_HISTORIC_HOURS, ChronoUnit.HOURS));
440 private Instant getHourStart(Instant instant) {
441 return instant.truncatedTo(ChronoUnit.HOURS);