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.handler;
15 import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
16 import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
18 import java.math.BigDecimal;
19 import java.time.Duration;
20 import java.time.Instant;
21 import java.time.LocalDateTime;
22 import java.time.LocalTime;
23 import java.time.format.DateTimeFormatter;
24 import java.time.temporal.ChronoUnit;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Currency;
28 import java.util.HashMap;
29 import java.util.List;
31 import java.util.Map.Entry;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
36 import javax.measure.Unit;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.eclipse.jetty.client.HttpClient;
41 import org.eclipse.jetty.http.HttpStatus;
42 import org.openhab.binding.energidataservice.internal.ApiController;
43 import org.openhab.binding.energidataservice.internal.CacheManager;
44 import org.openhab.binding.energidataservice.internal.DatahubTariff;
45 import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions;
46 import org.openhab.binding.energidataservice.internal.api.ChargeType;
47 import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
48 import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
49 import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory;
50 import org.openhab.binding.energidataservice.internal.api.Dataset;
51 import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
52 import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
53 import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
54 import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecord;
55 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
56 import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
57 import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration;
58 import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration;
59 import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
60 import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory;
61 import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
62 import org.openhab.core.i18n.TimeZoneProvider;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.library.unit.CurrencyUnits;
66 import org.openhab.core.library.unit.Units;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.ThingHandlerService;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.State;
77 import org.openhab.core.types.TimeSeries;
78 import org.openhab.core.types.UnDefType;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
83 * The {@link EnergiDataServiceHandler} is responsible for handling commands, which are
84 * sent to one of the channels.
86 * @author Jacob Laursen - Initial contribution
89 public class EnergiDataServiceHandler extends BaseThingHandler {
91 private static final Duration emissionPrognosisJobInterval = Duration.ofMinutes(15);
92 private static final Duration emissionRealtimeJobInterval = Duration.ofMinutes(5);
94 private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class);
95 private final TimeZoneProvider timeZoneProvider;
96 private final ApiController apiController;
97 private final CacheManager cacheManager;
99 private EnergiDataServiceConfiguration config;
100 private RetryStrategy retryPolicy = RetryPolicyFactory.initial();
101 private boolean realtimeEmissionsFetchedFirstTime = false;
102 private @Nullable ScheduledFuture<?> refreshPriceFuture;
103 private @Nullable ScheduledFuture<?> refreshEmissionPrognosisFuture;
104 private @Nullable ScheduledFuture<?> refreshEmissionRealtimeFuture;
105 private @Nullable ScheduledFuture<?> priceUpdateFuture;
107 public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
109 this.timeZoneProvider = timeZoneProvider;
110 this.apiController = new ApiController(httpClient, timeZoneProvider);
111 this.cacheManager = new CacheManager();
113 // Default configuration
114 this.config = new EnergiDataServiceConfiguration();
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 if (!(command instanceof RefreshType)) {
123 String channelId = channelUID.getId();
124 if (ELECTRICITY_CHANNELS.contains(channelId)) {
125 refreshElectricityPrices();
126 } else if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelId)) {
127 rescheduleEmissionPrognosisJob();
128 } else if (CHANNEL_CO2_EMISSION_REALTIME.equals(channelId)) {
129 realtimeEmissionsFetchedFirstTime = false;
130 rescheduleEmissionRealtimeJob();
135 public void initialize() {
136 config = getConfigAs(EnergiDataServiceConfiguration.class);
138 if (config.priceArea.isBlank()) {
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
140 "@text/offline.conf-error.no-price-area");
143 GlobalLocationNumber gln = config.getGridCompanyGLN();
144 if (!gln.isEmpty() && !gln.isValid()) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
146 "@text/offline.conf-error.invalid-grid-company-gln");
149 gln = config.getEnerginetGLN();
150 if (!gln.isEmpty() && !gln.isValid()) {
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
152 "@text/offline.conf-error.invalid-energinet-gln");
156 updateStatus(ThingStatus.UNKNOWN);
158 refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS);
160 if (isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) {
161 rescheduleEmissionPrognosisJob();
163 if (isLinked(CHANNEL_CO2_EMISSION_REALTIME)) {
164 rescheduleEmissionRealtimeJob();
169 public void dispose() {
170 ScheduledFuture<?> refreshPriceFuture = this.refreshPriceFuture;
171 if (refreshPriceFuture != null) {
172 refreshPriceFuture.cancel(true);
173 this.refreshPriceFuture = null;
175 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
176 if (refreshEmissionPrognosisFuture != null) {
177 refreshEmissionPrognosisFuture.cancel(true);
178 this.refreshEmissionPrognosisFuture = null;
180 ScheduledFuture<?> refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture;
181 if (refreshEmissionRealtimeFuture != null) {
182 refreshEmissionRealtimeFuture.cancel(true);
183 this.refreshEmissionRealtimeFuture = null;
185 ScheduledFuture<?> priceUpdateFuture = this.priceUpdateFuture;
186 if (priceUpdateFuture != null) {
187 priceUpdateFuture.cancel(true);
188 this.priceUpdateFuture = null;
191 cacheManager.clear();
195 public Collection<Class<? extends ThingHandlerService>> getServices() {
196 return Set.of(EnergiDataServiceActions.class);
200 public void channelLinked(ChannelUID channelUID) {
201 super.channelLinked(channelUID);
203 if (!"DK1".equals(config.priceArea) && !"DK2".equals(config.priceArea)
204 && (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelUID.getId())
205 || CHANNEL_CO2_EMISSION_REALTIME.contains(channelUID.getId()))) {
206 logger.warn("Item linked to channel '{}', but price area {} is not supported for this channel",
207 channelUID.getId(), config.priceArea);
212 public void channelUnlinked(ChannelUID channelUID) {
213 super.channelUnlinked(channelUID);
215 if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelUID.getId()) && !isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) {
216 logger.debug("No more items linked to channel '{}', stopping emission prognosis refresh job",
218 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
219 if (refreshEmissionPrognosisFuture != null) {
220 refreshEmissionPrognosisFuture.cancel(true);
221 this.refreshEmissionPrognosisFuture = null;
223 } else if (CHANNEL_CO2_EMISSION_REALTIME.contains(channelUID.getId())
224 && !isLinked(CHANNEL_CO2_EMISSION_REALTIME)) {
225 logger.debug("No more items linked to channel '{}', stopping realtime emission refresh job",
227 ScheduledFuture<?> refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture;
228 if (refreshEmissionRealtimeFuture != null) {
229 refreshEmissionRealtimeFuture.cancel(true);
230 this.refreshEmissionRealtimeFuture = null;
235 private void refreshElectricityPrices() {
236 RetryStrategy retryPolicy;
238 if (isLinked(CHANNEL_SPOT_PRICE)) {
239 downloadSpotPrices();
242 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
243 if (isLinked(datahubTariff.getChannelId())) {
244 downloadTariffs(datahubTariff);
248 updateStatus(ThingStatus.ONLINE);
252 if (isLinked(CHANNEL_SPOT_PRICE)) {
253 long numberOfFutureSpotPrices = cacheManager.getNumberOfFutureSpotPrices();
254 LocalTime now = LocalTime.now(NORD_POOL_TIMEZONE);
256 if (numberOfFutureSpotPrices >= 13 || (numberOfFutureSpotPrices == 12
257 && now.isAfter(DAILY_REFRESH_TIME_CET.minusHours(1)) && now.isBefore(DAILY_REFRESH_TIME_CET))) {
258 retryPolicy = RetryPolicyFactory.atFixedTime(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE);
260 logger.warn("Spot prices are not available, retry scheduled (see details in Thing properties)");
261 retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing();
264 retryPolicy = RetryPolicyFactory.atFixedTime(LocalTime.MIDNIGHT, timeZoneProvider.getTimeZone());
266 } catch (DataServiceException e) {
267 if (e.getHttpStatus() != 0) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
269 HttpStatus.getCode(e.getHttpStatus()).getMessage());
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
273 if (e.getCause() != null) {
274 logger.debug("Error retrieving prices", e);
276 retryPolicy = RetryPolicyFactory.fromThrowable(e);
277 } catch (InterruptedException e) {
278 logger.debug("Refresh job interrupted");
279 Thread.currentThread().interrupt();
283 reschedulePriceRefreshJob(retryPolicy);
286 private void downloadSpotPrices() throws InterruptedException, DataServiceException {
287 if (cacheManager.areSpotPricesFullyCached()) {
288 logger.debug("Cached spot prices still valid, skipping download.");
291 DateQueryParameter start;
292 if (cacheManager.areHistoricSpotPricesCached()) {
293 start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW);
295 start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
296 Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS));
298 Map<String, String> properties = editProperties();
300 ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(),
302 cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency());
304 updateProperties(properties);
308 private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedException, DataServiceException {
309 GlobalLocationNumber globalLocationNumber = switch (datahubTariff) {
310 case GRID_TARIFF -> config.getGridCompanyGLN();
311 default -> config.getEnerginetGLN();
313 if (globalLocationNumber.isEmpty()) {
316 if (cacheManager.areTariffsValidTomorrow(datahubTariff)) {
317 logger.debug("Cached tariffs of type {} still valid, skipping download.", datahubTariff);
318 cacheManager.updateTariffs(datahubTariff);
320 DatahubTariffFilter filter = switch (datahubTariff) {
321 case GRID_TARIFF -> getGridTariffFilter();
322 case SYSTEM_TARIFF -> DatahubTariffFilterFactory.getSystemTariff();
323 case TRANSMISSION_GRID_TARIFF -> DatahubTariffFilterFactory.getTransmissionGridTariff();
324 case ELECTRICITY_TAX -> DatahubTariffFilterFactory.getElectricityTax();
325 case REDUCED_ELECTRICITY_TAX -> DatahubTariffFilterFactory.getReducedElectricityTax();
327 cacheManager.putTariffs(datahubTariff, downloadPriceLists(globalLocationNumber, filter));
331 private Collection<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber,
332 DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
333 Map<String, String> properties = editProperties();
335 return apiController.getDatahubPriceLists(globalLocationNumber, ChargeType.Tariff, filter, properties);
337 updateProperties(properties);
341 private DatahubTariffFilter getGridTariffFilter() {
342 Channel channel = getThing().getChannel(CHANNEL_GRID_TARIFF);
343 if (channel == null) {
344 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
347 DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration()
348 .as(DatahubPriceConfiguration.class);
350 if (!datahubPriceConfiguration.hasAnyFilterOverrides()) {
351 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
354 DateQueryParameter start = datahubPriceConfiguration.getStart();
356 logger.warn("Invalid channel configuration parameter 'start' or 'offset': {} (offset: {})",
357 datahubPriceConfiguration.start, datahubPriceConfiguration.offset);
358 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
361 Set<ChargeTypeCode> chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes();
362 Set<String> notes = datahubPriceConfiguration.getNotes();
363 DatahubTariffFilter filter;
364 if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) {
365 // Completely override filter.
366 filter = new DatahubTariffFilter(chargeTypeCodes, notes, start);
368 // Only override start date in pre-configured filter.
369 filter = new DatahubTariffFilter(DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN),
373 return new DatahubTariffFilter(filter, DateQueryParameter.of(filter.getDateQueryParameter(),
374 Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS)));
377 private void refreshCo2EmissionPrognosis() {
379 updateCo2Emissions(Dataset.CO2EmissionPrognosis, CHANNEL_CO2_EMISSION_PROGNOSIS,
380 DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofMinutes(-5)));
381 updateStatus(ThingStatus.ONLINE);
382 } catch (DataServiceException e) {
383 if (e.getHttpStatus() != 0) {
384 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
385 HttpStatus.getCode(e.getHttpStatus()).getMessage());
387 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
389 if (e.getCause() != null) {
390 logger.debug("Error retrieving CO2 emission prognosis", e);
392 } catch (InterruptedException e) {
393 logger.debug("Emission prognosis refresh job interrupted");
394 Thread.currentThread().interrupt();
399 private void refreshCo2EmissionRealtime() {
401 updateCo2Emissions(Dataset.CO2Emission, CHANNEL_CO2_EMISSION_REALTIME,
402 DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
403 realtimeEmissionsFetchedFirstTime ? Duration.ofMinutes(-5) : Duration.ofHours(-24)));
404 realtimeEmissionsFetchedFirstTime = true;
405 updateStatus(ThingStatus.ONLINE);
406 } catch (DataServiceException e) {
407 if (e.getHttpStatus() != 0) {
408 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
409 HttpStatus.getCode(e.getHttpStatus()).getMessage());
411 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
413 if (e.getCause() != null) {
414 logger.debug("Error retrieving CO2 realtime emissions", e);
416 } catch (InterruptedException e) {
417 logger.debug("Emission realtime refresh job interrupted");
418 Thread.currentThread().interrupt();
423 private void updateCo2Emissions(Dataset dataset, String channelId, DateQueryParameter dateQueryParameter)
424 throws InterruptedException, DataServiceException {
425 if (!"DK1".equals(config.priceArea) && !"DK2".equals(config.priceArea)) {
426 // Dataset is only for Denmark.
429 Map<String, String> properties = editProperties();
430 CO2EmissionRecord[] emissionRecords = apiController.getCo2Emissions(dataset, config.priceArea,
431 dateQueryParameter, properties);
432 updateProperties(properties);
434 TimeSeries timeSeries = new TimeSeries(REPLACE);
435 Instant now = Instant.now();
437 if (dataset == Dataset.CO2Emission && emissionRecords.length > 0) {
438 // Records are sorted descending, first record is current.
439 updateState(channelId, new QuantityType<>(emissionRecords[0].emission(), Units.GRAM_PER_KILOWATT_HOUR));
442 for (CO2EmissionRecord emissionRecord : emissionRecords) {
443 State state = new QuantityType<>(emissionRecord.emission(), Units.GRAM_PER_KILOWATT_HOUR);
444 timeSeries.add(emissionRecord.start(), state);
446 if (dataset == Dataset.CO2EmissionPrognosis && now.compareTo(emissionRecord.start()) >= 0
447 && now.compareTo(emissionRecord.end()) < 0) {
448 updateState(channelId, state);
451 sendTimeSeries(channelId, timeSeries);
454 private void updatePrices() {
455 cacheManager.cleanup();
457 updateCurrentSpotPrice();
458 Arrays.stream(DatahubTariff.values())
459 .forEach(tariff -> updateCurrentTariff(tariff.getChannelId(), cacheManager.getTariff(tariff)));
461 reschedulePriceUpdateJob();
464 private void updateCurrentSpotPrice() {
465 if (!isLinked(CHANNEL_SPOT_PRICE)) {
468 BigDecimal spotPrice = cacheManager.getSpotPrice();
469 updatePriceState(CHANNEL_SPOT_PRICE, spotPrice, config.getCurrency());
472 private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) {
473 if (!isLinked(channelId)) {
476 updatePriceState(channelId, tariff, CURRENCY_DKK);
479 private void updatePriceState(String channelID, @Nullable BigDecimal price, Currency currency) {
480 updateState(channelID, price != null ? getEnergyPrice(price, currency) : UnDefType.UNDEF);
483 private State getEnergyPrice(BigDecimal price, Currency currency) {
484 String currencyCode = currency.getCurrencyCode();
485 Unit<?> unit = CurrencyUnits.getInstance().getUnit(currencyCode);
487 logger.trace("Currency {} is unknown, falling back to DecimalType", currency.getCurrencyCode());
488 return new DecimalType(price);
491 return new QuantityType<>(price + " " + currencyCode + "/kWh");
492 } catch (IllegalArgumentException e) {
493 logger.debug("Unable to create QuantityType, falling back to DecimalType", e);
494 return new DecimalType(price);
498 private void updateTimeSeries() {
499 TimeSeries spotPriceTimeSeries = new TimeSeries(REPLACE);
500 Map<DatahubTariff, TimeSeries> datahubTimeSeriesMap = new HashMap<>();
501 Map<DatahubTariff, BigDecimal> datahubPreviousTariff = new HashMap<>();
502 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
503 datahubTimeSeriesMap.put(datahubTariff, new TimeSeries(REPLACE));
506 Map<Instant, BigDecimal> spotPriceMap = cacheManager.getSpotPrices();
507 List<Entry<Instant, BigDecimal>> spotPrices = spotPriceMap.entrySet().stream()
508 .sorted(Map.Entry.comparingByKey()).toList();
509 for (Entry<Instant, BigDecimal> spotPrice : spotPrices) {
510 Instant hourStart = spotPrice.getKey();
511 if (isLinked(CHANNEL_SPOT_PRICE)) {
512 spotPriceTimeSeries.add(hourStart, getEnergyPrice(spotPrice.getValue(), config.getCurrency()));
514 for (Map.Entry<DatahubTariff, TimeSeries> entry : datahubTimeSeriesMap.entrySet()) {
515 DatahubTariff datahubTariff = entry.getKey();
516 String channelId = datahubTariff.getChannelId();
517 if (!isLinked(channelId)) {
520 BigDecimal tariff = cacheManager.getTariff(datahubTariff, hourStart);
521 if (tariff != null) {
522 BigDecimal previousTariff = datahubPreviousTariff.get(datahubTariff);
523 if (previousTariff != null && tariff.equals(previousTariff)) {
524 // Skip redundant states.
527 TimeSeries timeSeries = entry.getValue();
528 timeSeries.add(hourStart, getEnergyPrice(tariff, CURRENCY_DKK));
529 datahubPreviousTariff.put(datahubTariff, tariff);
533 if (spotPriceTimeSeries.size() > 0) {
534 sendTimeSeries(CHANNEL_SPOT_PRICE, spotPriceTimeSeries);
536 for (Map.Entry<DatahubTariff, TimeSeries> entry : datahubTimeSeriesMap.entrySet()) {
537 DatahubTariff datahubTariff = entry.getKey();
538 String channelId = datahubTariff.getChannelId();
539 if (!isLinked(channelId)) {
542 TimeSeries timeSeries = entry.getValue();
543 if (timeSeries.size() > 0) {
544 sendTimeSeries(channelId, timeSeries);
550 * Get the configured {@link Currency} for spot prices.
552 * @return Spot price currency
554 public Currency getCurrency() {
555 return config.getCurrency();
559 * Get cached spot prices or try once to download them if not cached
560 * (usually if no items are linked).
562 * @return Map of future spot prices
564 public Map<Instant, BigDecimal> getSpotPrices() {
566 downloadSpotPrices();
567 } catch (DataServiceException e) {
568 if (logger.isDebugEnabled()) {
569 logger.warn("Error retrieving spot prices", e);
571 logger.warn("Error retrieving spot prices: {}", e.getMessage());
573 } catch (InterruptedException e) {
574 Thread.currentThread().interrupt();
577 return cacheManager.getSpotPrices();
581 * Return cached tariffs or try once to download them if not cached
582 * (usually if no items are linked).
584 * @return Map of future tariffs
586 public Map<Instant, BigDecimal> getTariffs(DatahubTariff datahubTariff) {
588 downloadTariffs(datahubTariff);
589 } catch (DataServiceException e) {
590 if (logger.isDebugEnabled()) {
591 logger.warn("Error retrieving tariffs", e);
593 logger.warn("Error retrieving tariffs of type {}: {}", datahubTariff, e.getMessage());
595 } catch (InterruptedException e) {
596 Thread.currentThread().interrupt();
599 return cacheManager.getTariffs(datahubTariff);
603 * Return whether reduced electricity tax is set in configuration.
605 * @return true if reduced electricity tax applies
607 public boolean isReducedElectricityTax() {
608 return config.reducedElectricityTax;
611 private void reschedulePriceUpdateJob() {
612 ScheduledFuture<?> priceUpdateJob = this.priceUpdateFuture;
613 if (priceUpdateJob != null) {
614 // Do not interrupt ourselves.
615 priceUpdateJob.cancel(false);
616 this.priceUpdateFuture = null;
619 Instant now = Instant.now();
620 long millisUntilNextClockHour = Duration
621 .between(now, now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS)).toMillis() + 1;
622 this.priceUpdateFuture = scheduler.schedule(this::updatePrices, millisUntilNextClockHour,
623 TimeUnit.MILLISECONDS);
624 logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour);
627 private void reschedulePriceRefreshJob(RetryStrategy retryPolicy) {
628 // Preserve state of previous retry policy when configuration is the same.
629 if (!retryPolicy.equals(this.retryPolicy)) {
630 this.retryPolicy = retryPolicy;
633 ScheduledFuture<?> refreshJob = this.refreshPriceFuture;
635 long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds();
636 Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh);
637 this.refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh,
639 logger.debug("Price refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh);
640 DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
641 updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone())
642 .truncatedTo(ChronoUnit.SECONDS).format(formatter));
644 if (refreshJob != null) {
645 refreshJob.cancel(true);
649 private void rescheduleEmissionPrognosisJob() {
650 logger.debug("Scheduling emission prognosis refresh job now and every {}", emissionPrognosisJobInterval);
652 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
653 if (refreshEmissionPrognosisFuture != null) {
654 refreshEmissionPrognosisFuture.cancel(true);
657 this.refreshEmissionPrognosisFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionPrognosis, 0,
658 emissionPrognosisJobInterval.toSeconds(), TimeUnit.SECONDS);
661 private void rescheduleEmissionRealtimeJob() {
662 logger.debug("Scheduling emission realtime refresh job now and every {}", emissionRealtimeJobInterval);
664 ScheduledFuture<?> refreshEmissionFuture = this.refreshEmissionRealtimeFuture;
665 if (refreshEmissionFuture != null) {
666 refreshEmissionFuture.cancel(true);
669 this.refreshEmissionRealtimeFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionRealtime, 0,
670 emissionRealtimeJobInterval.toSeconds(), TimeUnit.SECONDS);