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.List;
30 import java.util.Map.Entry;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
35 import javax.measure.Unit;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.openhab.binding.energidataservice.internal.ApiController;
42 import org.openhab.binding.energidataservice.internal.CacheManager;
43 import org.openhab.binding.energidataservice.internal.DatahubTariff;
44 import org.openhab.binding.energidataservice.internal.action.EnergiDataServiceActions;
45 import org.openhab.binding.energidataservice.internal.api.ChargeType;
46 import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode;
47 import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
48 import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory;
49 import org.openhab.binding.energidataservice.internal.api.Dataset;
50 import org.openhab.binding.energidataservice.internal.api.DateQueryParameter;
51 import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
52 import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber;
53 import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecord;
54 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord;
55 import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
56 import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration;
57 import org.openhab.binding.energidataservice.internal.config.EnergiDataServiceConfiguration;
58 import org.openhab.binding.energidataservice.internal.exception.DataServiceException;
59 import org.openhab.binding.energidataservice.internal.retry.RetryPolicyFactory;
60 import org.openhab.binding.energidataservice.internal.retry.RetryStrategy;
61 import org.openhab.core.i18n.TimeZoneProvider;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.QuantityType;
64 import org.openhab.core.library.unit.CurrencyUnits;
65 import org.openhab.core.library.unit.Units;
66 import org.openhab.core.thing.Channel;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.openhab.core.types.TimeSeries;
77 import org.openhab.core.types.UnDefType;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
82 * The {@link EnergiDataServiceHandler} is responsible for handling commands, which are
83 * sent to one of the channels.
85 * @author Jacob Laursen - Initial contribution
88 public class EnergiDataServiceHandler extends BaseThingHandler {
90 private static final Duration emissionPrognosisJobInterval = Duration.ofMinutes(15);
91 private static final Duration emissionRealtimeJobInterval = Duration.ofMinutes(5);
93 private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class);
94 private final TimeZoneProvider timeZoneProvider;
95 private final ApiController apiController;
96 private final CacheManager cacheManager;
98 private EnergiDataServiceConfiguration config;
99 private RetryStrategy retryPolicy = RetryPolicyFactory.initial();
100 private boolean realtimeEmissionsFetchedFirstTime = false;
101 private @Nullable ScheduledFuture<?> refreshPriceFuture;
102 private @Nullable ScheduledFuture<?> refreshEmissionPrognosisFuture;
103 private @Nullable ScheduledFuture<?> refreshEmissionRealtimeFuture;
104 private @Nullable ScheduledFuture<?> priceUpdateFuture;
106 public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
108 this.timeZoneProvider = timeZoneProvider;
109 this.apiController = new ApiController(httpClient, timeZoneProvider);
110 this.cacheManager = new CacheManager();
112 // Default configuration
113 this.config = new EnergiDataServiceConfiguration();
117 public void handleCommand(ChannelUID channelUID, Command command) {
118 if (!(command instanceof RefreshType)) {
122 String channelId = channelUID.getId();
123 if (ELECTRICITY_CHANNELS.contains(channelId)) {
124 refreshElectricityPrices();
125 } else if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelId)) {
126 rescheduleEmissionPrognosisJob();
127 } else if (CHANNEL_CO2_EMISSION_REALTIME.equals(channelId)) {
128 realtimeEmissionsFetchedFirstTime = false;
129 rescheduleEmissionRealtimeJob();
134 public void initialize() {
135 config = getConfigAs(EnergiDataServiceConfiguration.class);
137 if (config.priceArea.isBlank()) {
138 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
139 "@text/offline.conf-error.no-price-area");
142 GlobalLocationNumber gln = config.getGridCompanyGLN();
143 if (!gln.isEmpty() && !gln.isValid()) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
145 "@text/offline.conf-error.invalid-grid-company-gln");
148 gln = config.getEnerginetGLN();
149 if (!gln.isEmpty() && !gln.isValid()) {
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
151 "@text/offline.conf-error.invalid-energinet-gln");
155 updateStatus(ThingStatus.UNKNOWN);
157 refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS);
159 if (isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) {
160 rescheduleEmissionPrognosisJob();
162 if (isLinked(CHANNEL_CO2_EMISSION_REALTIME)) {
163 rescheduleEmissionRealtimeJob();
168 public void dispose() {
169 ScheduledFuture<?> refreshPriceFuture = this.refreshPriceFuture;
170 if (refreshPriceFuture != null) {
171 refreshPriceFuture.cancel(true);
172 this.refreshPriceFuture = null;
174 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
175 if (refreshEmissionPrognosisFuture != null) {
176 refreshEmissionPrognosisFuture.cancel(true);
177 this.refreshEmissionPrognosisFuture = null;
179 ScheduledFuture<?> refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture;
180 if (refreshEmissionRealtimeFuture != null) {
181 refreshEmissionRealtimeFuture.cancel(true);
182 this.refreshEmissionRealtimeFuture = null;
184 ScheduledFuture<?> priceUpdateFuture = this.priceUpdateFuture;
185 if (priceUpdateFuture != null) {
186 priceUpdateFuture.cancel(true);
187 this.priceUpdateFuture = null;
190 cacheManager.clear();
194 public Collection<Class<? extends ThingHandlerService>> getServices() {
195 return Set.of(EnergiDataServiceActions.class);
199 public void channelLinked(ChannelUID channelUID) {
200 super.channelLinked(channelUID);
202 if (!"DK1".equals(config.priceArea) && !"DK2".equals(config.priceArea)
203 && (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelUID.getId())
204 || CHANNEL_CO2_EMISSION_REALTIME.contains(channelUID.getId()))) {
205 logger.warn("Item linked to channel '{}', but price area {} is not supported for this channel",
206 channelUID.getId(), config.priceArea);
211 public void channelUnlinked(ChannelUID channelUID) {
212 super.channelUnlinked(channelUID);
214 if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelUID.getId()) && !isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) {
215 logger.debug("No more items linked to channel '{}', stopping emission prognosis refresh job",
217 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
218 if (refreshEmissionPrognosisFuture != null) {
219 refreshEmissionPrognosisFuture.cancel(true);
220 this.refreshEmissionPrognosisFuture = null;
222 } else if (CHANNEL_CO2_EMISSION_REALTIME.contains(channelUID.getId())
223 && !isLinked(CHANNEL_CO2_EMISSION_REALTIME)) {
224 logger.debug("No more items linked to channel '{}', stopping realtime emission refresh job",
226 ScheduledFuture<?> refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture;
227 if (refreshEmissionRealtimeFuture != null) {
228 refreshEmissionRealtimeFuture.cancel(true);
229 this.refreshEmissionRealtimeFuture = null;
234 private void refreshElectricityPrices() {
235 RetryStrategy retryPolicy;
237 if (isLinked(CHANNEL_SPOT_PRICE)) {
238 downloadSpotPrices();
241 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
242 if (isLinked(datahubTariff.getChannelId())) {
243 downloadTariffs(datahubTariff);
247 updateStatus(ThingStatus.ONLINE);
251 if (isLinked(CHANNEL_SPOT_PRICE)) {
252 long numberOfFutureSpotPrices = cacheManager.getNumberOfFutureSpotPrices();
253 LocalTime now = LocalTime.now(NORD_POOL_TIMEZONE);
255 if (numberOfFutureSpotPrices >= 13 || (numberOfFutureSpotPrices == 12
256 && now.isAfter(DAILY_REFRESH_TIME_CET.minusHours(1)) && now.isBefore(DAILY_REFRESH_TIME_CET))) {
257 retryPolicy = RetryPolicyFactory.atFixedTime(DAILY_REFRESH_TIME_CET, NORD_POOL_TIMEZONE);
259 logger.warn("Spot prices are not available, retry scheduled (see details in Thing properties)");
260 retryPolicy = RetryPolicyFactory.whenExpectedSpotPriceDataMissing();
263 retryPolicy = RetryPolicyFactory.atFixedTime(LocalTime.MIDNIGHT, timeZoneProvider.getTimeZone());
265 } catch (DataServiceException e) {
266 if (e.getHttpStatus() != 0) {
267 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
268 HttpStatus.getCode(e.getHttpStatus()).getMessage());
270 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
272 if (e.getCause() != null) {
273 logger.debug("Error retrieving prices", e);
275 retryPolicy = RetryPolicyFactory.fromThrowable(e);
276 } catch (InterruptedException e) {
277 logger.debug("Refresh job interrupted");
278 Thread.currentThread().interrupt();
282 reschedulePriceRefreshJob(retryPolicy);
285 private void downloadSpotPrices() throws InterruptedException, DataServiceException {
286 if (cacheManager.areSpotPricesFullyCached()) {
287 logger.debug("Cached spot prices still valid, skipping download.");
290 DateQueryParameter start;
291 if (cacheManager.areHistoricSpotPricesCached()) {
292 start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW);
294 start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
295 Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS));
297 Map<String, String> properties = editProperties();
299 ElspotpriceRecord[] spotPriceRecords = apiController.getSpotPrices(config.priceArea, config.getCurrency(),
301 cacheManager.putSpotPrices(spotPriceRecords, config.getCurrency());
303 updateProperties(properties);
307 private void downloadTariffs(DatahubTariff datahubTariff) throws InterruptedException, DataServiceException {
308 GlobalLocationNumber globalLocationNumber = switch (datahubTariff) {
309 case GRID_TARIFF -> config.getGridCompanyGLN();
310 default -> config.getEnerginetGLN();
312 if (globalLocationNumber.isEmpty()) {
315 if (cacheManager.areTariffsValidTomorrow(datahubTariff)) {
316 logger.debug("Cached tariffs of type {} still valid, skipping download.", datahubTariff);
317 cacheManager.updateTariffs(datahubTariff);
319 DatahubTariffFilter filter = switch (datahubTariff) {
320 case GRID_TARIFF -> getGridTariffFilter();
321 case SYSTEM_TARIFF -> DatahubTariffFilterFactory.getSystemTariff();
322 case TRANSMISSION_GRID_TARIFF -> DatahubTariffFilterFactory.getTransmissionGridTariff();
323 case ELECTRICITY_TAX -> DatahubTariffFilterFactory.getElectricityTax();
324 case REDUCED_ELECTRICITY_TAX -> DatahubTariffFilterFactory.getReducedElectricityTax();
326 cacheManager.putTariffs(datahubTariff, downloadPriceLists(globalLocationNumber, filter));
330 private Collection<DatahubPricelistRecord> downloadPriceLists(GlobalLocationNumber globalLocationNumber,
331 DatahubTariffFilter filter) throws InterruptedException, DataServiceException {
332 Map<String, String> properties = editProperties();
334 return apiController.getDatahubPriceLists(globalLocationNumber, ChargeType.Tariff, filter, properties);
336 updateProperties(properties);
340 private DatahubTariffFilter getGridTariffFilter() {
341 Channel channel = getThing().getChannel(CHANNEL_GRID_TARIFF);
342 if (channel == null) {
343 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
346 DatahubPriceConfiguration datahubPriceConfiguration = channel.getConfiguration()
347 .as(DatahubPriceConfiguration.class);
349 if (!datahubPriceConfiguration.hasAnyFilterOverrides()) {
350 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
353 DateQueryParameter start = datahubPriceConfiguration.getStart();
355 logger.warn("Invalid channel configuration parameter 'start' or 'offset': {} (offset: {})",
356 datahubPriceConfiguration.start, datahubPriceConfiguration.offset);
357 return DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN);
360 Set<ChargeTypeCode> chargeTypeCodes = datahubPriceConfiguration.getChargeTypeCodes();
361 Set<String> notes = datahubPriceConfiguration.getNotes();
362 DatahubTariffFilter filter;
363 if (!chargeTypeCodes.isEmpty() || !notes.isEmpty()) {
364 // Completely override filter.
365 filter = new DatahubTariffFilter(chargeTypeCodes, notes, start);
367 // Only override start date in pre-configured filter.
368 filter = new DatahubTariffFilter(DatahubTariffFilterFactory.getGridTariffByGLN(config.gridCompanyGLN),
372 return new DatahubTariffFilter(filter, DateQueryParameter.of(filter.getDateQueryParameter(),
373 Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS)));
376 private void refreshCo2EmissionPrognosis() {
378 updateCo2Emissions(Dataset.CO2EmissionPrognosis, CHANNEL_CO2_EMISSION_PROGNOSIS,
379 DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofMinutes(-5)));
380 updateStatus(ThingStatus.ONLINE);
381 } catch (DataServiceException e) {
382 if (e.getHttpStatus() != 0) {
383 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
384 HttpStatus.getCode(e.getHttpStatus()).getMessage());
386 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
388 if (e.getCause() != null) {
389 logger.debug("Error retrieving CO2 emission prognosis", e);
391 } catch (InterruptedException e) {
392 logger.debug("Emission prognosis refresh job interrupted");
393 Thread.currentThread().interrupt();
398 private void refreshCo2EmissionRealtime() {
400 updateCo2Emissions(Dataset.CO2Emission, CHANNEL_CO2_EMISSION_REALTIME,
401 DateQueryParameter.of(DateQueryParameterType.UTC_NOW,
402 realtimeEmissionsFetchedFirstTime ? Duration.ofMinutes(-5) : Duration.ofHours(-24)));
403 realtimeEmissionsFetchedFirstTime = true;
404 updateStatus(ThingStatus.ONLINE);
405 } catch (DataServiceException e) {
406 if (e.getHttpStatus() != 0) {
407 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
408 HttpStatus.getCode(e.getHttpStatus()).getMessage());
410 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
412 if (e.getCause() != null) {
413 logger.debug("Error retrieving CO2 realtime emissions", e);
415 } catch (InterruptedException e) {
416 logger.debug("Emission realtime refresh job interrupted");
417 Thread.currentThread().interrupt();
422 private void updateCo2Emissions(Dataset dataset, String channelId, DateQueryParameter dateQueryParameter)
423 throws InterruptedException, DataServiceException {
424 if (!"DK1".equals(config.priceArea) && !"DK2".equals(config.priceArea)) {
425 // Dataset is only for Denmark.
428 Map<String, String> properties = editProperties();
429 CO2EmissionRecord[] emissionRecords = apiController.getCo2Emissions(dataset, config.priceArea,
430 dateQueryParameter, properties);
431 updateProperties(properties);
433 TimeSeries timeSeries = new TimeSeries(REPLACE);
434 Instant now = Instant.now();
436 if (dataset == Dataset.CO2Emission && emissionRecords.length > 0) {
437 // Records are sorted descending, first record is current.
438 updateState(channelId, new QuantityType<>(emissionRecords[0].emission(), Units.GRAM_PER_KILOWATT_HOUR));
441 for (CO2EmissionRecord emissionRecord : emissionRecords) {
442 State state = new QuantityType<>(emissionRecord.emission(), Units.GRAM_PER_KILOWATT_HOUR);
443 timeSeries.add(emissionRecord.start(), state);
445 if (dataset == Dataset.CO2EmissionPrognosis && now.compareTo(emissionRecord.start()) >= 0
446 && now.compareTo(emissionRecord.end()) < 0) {
447 updateState(channelId, state);
450 sendTimeSeries(channelId, timeSeries);
453 private void updatePrices() {
454 cacheManager.cleanup();
456 updateCurrentSpotPrice();
457 Arrays.stream(DatahubTariff.values())
458 .forEach(tariff -> updateCurrentTariff(tariff.getChannelId(), cacheManager.getTariff(tariff)));
460 reschedulePriceUpdateJob();
463 private void updateCurrentSpotPrice() {
464 if (!isLinked(CHANNEL_SPOT_PRICE)) {
467 BigDecimal spotPrice = cacheManager.getSpotPrice();
468 updatePriceState(CHANNEL_SPOT_PRICE, spotPrice, config.getCurrency());
471 private void updateCurrentTariff(String channelId, @Nullable BigDecimal tariff) {
472 if (!isLinked(channelId)) {
475 updatePriceState(channelId, tariff, CURRENCY_DKK);
478 private void updatePriceState(String channelID, @Nullable BigDecimal price, Currency currency) {
479 updateState(channelID, price != null ? getEnergyPrice(price, currency) : UnDefType.UNDEF);
482 private State getEnergyPrice(BigDecimal price, Currency currency) {
483 String currencyCode = currency.getCurrencyCode();
484 Unit<?> unit = CurrencyUnits.getInstance().getUnit(currencyCode);
486 logger.trace("Currency {} is unknown, falling back to DecimalType", currency.getCurrencyCode());
487 return new DecimalType(price);
490 return new QuantityType<>(price + " " + currencyCode + "/kWh");
491 } catch (IllegalArgumentException e) {
492 logger.debug("Unable to create QuantityType, falling back to DecimalType", e);
493 return new DecimalType(price);
497 private void updateTimeSeries() {
498 updatePriceTimeSeries(CHANNEL_SPOT_PRICE, cacheManager.getSpotPrices(), config.getCurrency(), false);
500 for (DatahubTariff datahubTariff : DatahubTariff.values()) {
501 String channelId = datahubTariff.getChannelId();
502 updatePriceTimeSeries(channelId, cacheManager.getTariffs(datahubTariff), CURRENCY_DKK, true);
506 private void updatePriceTimeSeries(String channelId, Map<Instant, BigDecimal> priceMap, Currency currency,
507 boolean deduplicate) {
508 if (!isLinked(channelId)) {
511 List<Entry<Instant, BigDecimal>> prices = priceMap.entrySet().stream().sorted(Map.Entry.comparingByKey())
513 TimeSeries timeSeries = new TimeSeries(REPLACE);
514 BigDecimal previousTariff = null;
515 for (Entry<Instant, BigDecimal> price : prices) {
516 Instant hourStart = price.getKey();
517 BigDecimal priceValue = price.getValue();
518 if (deduplicate && priceValue.equals(previousTariff)) {
519 // Skip redundant states.
522 timeSeries.add(hourStart, getEnergyPrice(priceValue, currency));
523 previousTariff = priceValue;
525 if (timeSeries.size() > 0) {
526 sendTimeSeries(channelId, timeSeries);
531 * Get the configured {@link Currency} for spot prices.
533 * @return Spot price currency
535 public Currency getCurrency() {
536 return config.getCurrency();
540 * Get cached spot prices or try once to download them if not cached
541 * (usually if no items are linked).
543 * @return Map of future spot prices
545 public Map<Instant, BigDecimal> getSpotPrices() {
547 downloadSpotPrices();
548 } catch (DataServiceException e) {
549 if (logger.isDebugEnabled()) {
550 logger.warn("Error retrieving spot prices", e);
552 logger.warn("Error retrieving spot prices: {}", e.getMessage());
554 } catch (InterruptedException e) {
555 Thread.currentThread().interrupt();
558 return cacheManager.getSpotPrices();
562 * Return cached tariffs or try once to download them if not cached
563 * (usually if no items are linked).
565 * @return Map of future tariffs
567 public Map<Instant, BigDecimal> getTariffs(DatahubTariff datahubTariff) {
569 downloadTariffs(datahubTariff);
570 } catch (DataServiceException e) {
571 if (logger.isDebugEnabled()) {
572 logger.warn("Error retrieving tariffs", e);
574 logger.warn("Error retrieving tariffs of type {}: {}", datahubTariff, e.getMessage());
576 } catch (InterruptedException e) {
577 Thread.currentThread().interrupt();
580 return cacheManager.getTariffs(datahubTariff);
584 * Return whether reduced electricity tax is set in configuration.
586 * @return true if reduced electricity tax applies
588 public boolean isReducedElectricityTax() {
589 return config.reducedElectricityTax;
592 private void reschedulePriceUpdateJob() {
593 ScheduledFuture<?> priceUpdateJob = this.priceUpdateFuture;
594 if (priceUpdateJob != null) {
595 // Do not interrupt ourselves.
596 priceUpdateJob.cancel(false);
597 this.priceUpdateFuture = null;
600 Instant now = Instant.now();
601 long millisUntilNextClockHour = Duration
602 .between(now, now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS)).toMillis() + 1;
603 this.priceUpdateFuture = scheduler.schedule(this::updatePrices, millisUntilNextClockHour,
604 TimeUnit.MILLISECONDS);
605 logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour);
608 private void reschedulePriceRefreshJob(RetryStrategy retryPolicy) {
609 // Preserve state of previous retry policy when configuration is the same.
610 if (!retryPolicy.equals(this.retryPolicy)) {
611 this.retryPolicy = retryPolicy;
614 ScheduledFuture<?> refreshJob = this.refreshPriceFuture;
616 long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds();
617 Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh);
618 this.refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh,
620 logger.debug("Price refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh);
621 DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
622 updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone())
623 .truncatedTo(ChronoUnit.SECONDS).format(formatter));
625 if (refreshJob != null) {
626 refreshJob.cancel(true);
630 private void rescheduleEmissionPrognosisJob() {
631 logger.debug("Scheduling emission prognosis refresh job now and every {}", emissionPrognosisJobInterval);
633 ScheduledFuture<?> refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture;
634 if (refreshEmissionPrognosisFuture != null) {
635 refreshEmissionPrognosisFuture.cancel(true);
638 this.refreshEmissionPrognosisFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionPrognosis, 0,
639 emissionPrognosisJobInterval.toSeconds(), TimeUnit.SECONDS);
642 private void rescheduleEmissionRealtimeJob() {
643 logger.debug("Scheduling emission realtime refresh job now and every {}", emissionRealtimeJobInterval);
645 ScheduledFuture<?> refreshEmissionFuture = this.refreshEmissionRealtimeFuture;
646 if (refreshEmissionFuture != null) {
647 refreshEmissionFuture.cancel(true);
650 this.refreshEmissionRealtimeFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionRealtime, 0,
651 emissionRealtimeJobInterval.toSeconds(), TimeUnit.SECONDS);