diff --git a/bundles/org.openhab.binding.energidataservice/README.md b/bundles/org.openhab.binding.energidataservice/README.md index cff534c805a..05e5ab6ae54 100644 --- a/bundles/org.openhab.binding.energidataservice/README.md +++ b/bundles/org.openhab.binding.energidataservice/README.md @@ -47,22 +47,24 @@ It will not impact channels, see [Electricity Tax](#electricity-tax) for further ### Channel Group `electricity` -| Channel | Type | Description | Advanced | -|--------------------------|--------------------|--------------------------------------------------------------------------------|----------| -| spot-price | Number:EnergyPrice | Spot price in DKK or EUR per kWh | no | -| grid-tariff | Number:EnergyPrice | Grid tariff in DKK per kWh. Only available when `gridCompanyGLN` is configured | no | -| system-tariff | Number:EnergyPrice | System tariff in DKK per kWh | no | -| transmission-grid-tariff | Number:EnergyPrice | Transmission grid tariff in DKK per kWh | no | -| electricity-tax | Number:EnergyPrice | Electricity tax in DKK per kWh | no | -| reduced-electricity-tax | Number:EnergyPrice | Reduced electricity tax in DKK per kWh. For electric heating customers only | no | +| Channel | Type | Description | +|--------------------------|--------------------------|----------------------------------------------------------------------------------------| +| spot-price | Number:EnergyPrice | Spot price in DKK or EUR per kWh | +| grid-tariff | Number:EnergyPrice | Grid tariff in DKK per kWh. Only available when `gridCompanyGLN` is configured | +| system-tariff | Number:EnergyPrice | System tariff in DKK per kWh | +| transmission-grid-tariff | Number:EnergyPrice | Transmission grid tariff in DKK per kWh | +| electricity-tax | Number:EnergyPrice | Electricity tax in DKK per kWh | +| reduced-electricity-tax | Number:EnergyPrice | Reduced electricity tax in DKK per kWh. For electric heating customers only | +| co2-emission-prognosis | Number:EmissionIntensity | Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh | +| co2-emission-realtime | Number:EmissionIntensity | Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh | _Please note:_ There is no channel providing the total price. Instead, create a group item with `SUM` as aggregate function and add the individual price items as children. This has the following advantages: -- Full customization possible: Freely choose the channels which should be included in the total. -- An additional item containing the kWh fee from your electricity supplier can be added also. -- Spot price can be configured in EUR while tariffs are in DKK. +- Full customization possible: Freely choose the channels which should be included in the total (even between different bindings). +- Spot price can be configured in EUR while tariffs are in DKK (and currency conversions are performed outside the binding). +- An additional item containing the kWh fee from your electricity supplier can be added also (and it can be dynamic). If you want electricity tax included in your total price, please add either `electricity-tax` or `reduced-electricity-tax` to the group - depending on which one applies. See [Electricity Tax](#electricity-tax) for further information. @@ -141,6 +143,17 @@ This reduced rate is made available through channel `reduced-electricity-tax`. The binding cannot determine or manage rate variations as they depend on metering data. Usually `reduced-electricity-tax` is preferred when using electricity for heating. +#### CO₂ Emissions + +Data for the CO₂ emission channels is published as time series with a resolution of 5 minutes. + +Channel `co2-emission-realtime` provides near up-to-date historic emission and is refreshed every 5 minutes. +When the binding is started, or a new item is linked, or a linked item receives an update command, historic data for the last 24 hours is provided in addition to the current value. + +Channel `co2-emission-prognosis` provides estimated prognosis for future emissions and is refreshed every 15 minutes. +Depending on the time of the day, an update of the prognosis may include estimates for more than 9 hours, but every update will have at least 9 hours into the future. +A persistence configuration is required for this channel. + ## Thing Actions Thing actions can be used to perform calculations as well as import prices directly into rules without relying on persistence. diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java index 42fa0a62174..ee80274a59e 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java @@ -38,8 +38,11 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.energidataservice.internal.api.ChargeType; import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; +import org.openhab.binding.energidataservice.internal.api.Dataset; import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; +import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecord; +import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecords; import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; @@ -66,9 +69,6 @@ public class ApiController { private static final String ENDPOINT = "https://api.energidataservice.dk/"; private static final String DATASET_PATH = "dataset/"; - private static final String DATASET_NAME_SPOT_PRICES = "Elspotprices"; - private static final String DATASET_NAME_DATAHUB_PRICELIST = "DatahubPricelist"; - private static final String FILTER_KEY_PRICE_AREA = "PriceArea"; private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType"; private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode"; @@ -111,7 +111,7 @@ public class ApiController { throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); } - Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_SPOT_PRICES) + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + Dataset.SpotPrices) .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) // .param("start", start.toString()) // .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") // @@ -119,23 +119,8 @@ public class ApiController { .agent(userAgent) // .method(HttpMethod.GET); - logger.trace("GET request for {}", request.getURI()); - try { - ContentResponse response = request.send(); - - updatePropertiesFromResponse(response, properties); - - int status = response.getStatus(); - if (!HttpStatus.isSuccess(status)) { - throw new DataServiceException("The request failed with HTTP error " + status, status); - } - String responseContent = response.getContentAsString(); - if (responseContent.isEmpty()) { - throw new DataServiceException("Empty response"); - } - logger.trace("Response content: '{}'", responseContent); - + String responseContent = sendRequest(request, properties); ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); if (records == null) { throw new DataServiceException("Error parsing response"); @@ -153,6 +138,27 @@ public class ApiController { } } + private String sendRequest(Request request, Map properties) + throws TimeoutException, ExecutionException, InterruptedException, DataServiceException { + logger.trace("GET request for {}", request.getURI()); + + ContentResponse response = request.send(); + + updatePropertiesFromResponse(response, properties); + + int status = response.getStatus(); + if (!HttpStatus.isSuccess(status)) { + throw new DataServiceException("The request failed with HTTP error " + status, status); + } + String responseContent = response.getContentAsString(); + if (responseContent.isEmpty()) { + throw new DataServiceException("Empty response"); + } + logger.trace("Response content: '{}'", responseContent); + + return responseContent; + } + private void updatePropertiesFromResponse(ContentResponse response, Map properties) { HttpFields headers = response.getHeaders(); String remainingCalls = headers.get(HEADER_REMAINING_CALLS); @@ -200,7 +206,7 @@ public class ApiController { filterMap.put(FILTER_KEY_NOTE, notes); } - Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + DATASET_NAME_DATAHUB_PRICELIST) + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + Dataset.DatahubPricelist) .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) // .param("filter", mapToFilter(filterMap)) // .param("columns", columns) // @@ -212,23 +218,8 @@ public class ApiController { request = request.param("start", dateQueryParameter.toString()); } - logger.trace("GET request for {}", request.getURI()); - try { - ContentResponse response = request.send(); - - updatePropertiesFromResponse(response, properties); - - int status = response.getStatus(); - if (!HttpStatus.isSuccess(status)) { - throw new DataServiceException("The request failed with HTTP error " + status, status); - } - String responseContent = response.getContentAsString(); - if (responseContent.isEmpty()) { - throw new DataServiceException("Empty response"); - } - logger.trace("Response content: '{}'", responseContent); - + String responseContent = sendRequest(request, properties); DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class); if (records == null) { throw new DataServiceException("Error parsing response"); @@ -255,4 +246,48 @@ public class ApiController { e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]") .collect(Collectors.joining(",")) + "}"; } + + /** + * Retrieve CO2 emissions for requested area. + * + * @param dataset Dataset to obtain + * @param priceArea Usually DK1 or DK2 + * @param start Specifies the start point of the period for the data request + * @param properties Map of properties which will be updated with metadata from headers + * @return Records with 5 minute periods and emissions in g/kWh. + * @throws InterruptedException + * @throws DataServiceException + */ + public CO2EmissionRecord[] getCo2Emissions(Dataset dataset, String priceArea, DateQueryParameter start, + Map properties) throws InterruptedException, DataServiceException { + if (dataset != Dataset.CO2Emission && dataset != Dataset.CO2EmissionPrognosis) { + throw new IllegalArgumentException("Invalid dataset " + dataset + " for getting CO2 emissions"); + } + Request request = httpClient.newRequest(ENDPOINT + DATASET_PATH + dataset) + .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) // + .param("start", start.toString()) // + .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") // + .param("columns", "Minutes5UTC,CO2Emission") // + .param("sort", "Minutes5UTC DESC") // + .agent(userAgent) // + .method(HttpMethod.GET); + + try { + String responseContent = sendRequest(request, properties); + CO2EmissionRecords records = gson.fromJson(responseContent, CO2EmissionRecords.class); + if (records == null) { + throw new DataServiceException("Error parsing response"); + } + + if (records.total() == 0 || Objects.isNull(records.records()) || records.records().length == 0) { + throw new DataServiceException("No records"); + } + + return Arrays.stream(records.records()).filter(Objects::nonNull).toArray(CO2EmissionRecord[]::new); + } catch (JsonSyntaxException e) { + throw new DataServiceException("Error parsing response", e); + } catch (TimeoutException | ExecutionException e) { + throw new DataServiceException(e); + } + } } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java index 4555a335b57..7147e822948 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/EnergiDataServiceBindingConstants.java @@ -52,6 +52,10 @@ public class EnergiDataServiceBindingConstants { + ChannelUID.CHANNEL_GROUP_SEPARATOR + "reduced-electricity-tax"; public static final String CHANNEL_TRANSMISSION_GRID_TARIFF = CHANNEL_GROUP_ELECTRICITY + ChannelUID.CHANNEL_GROUP_SEPARATOR + "transmission-grid-tariff"; + public static final String CHANNEL_CO2_EMISSION_PROGNOSIS = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "co2-emission-prognosis"; + public static final String CHANNEL_CO2_EMISSION_REALTIME = CHANNEL_GROUP_ELECTRICITY + + ChannelUID.CHANNEL_GROUP_SEPARATOR + "co2-emission-realtime"; public static final Set ELECTRICITY_CHANNELS = Set.of(CHANNEL_SPOT_PRICE, CHANNEL_GRID_TARIFF, CHANNEL_SYSTEM_TARIFF, CHANNEL_TRANSMISSION_GRID_TARIFF, CHANNEL_ELECTRICITY_TAX, diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/Dataset.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/Dataset.java new file mode 100644 index 00000000000..cd4e4574879 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/Dataset.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Energi Data Service dataset. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum Dataset { + SpotPrices("Elspotprices"), + DatahubPricelist("DatahubPricelist"), + CO2Emission("CO2Emis"), + CO2EmissionPrognosis("CO2EmisProg"); + + private final String name; + + Dataset(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecord.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecord.java new file mode 100644 index 00000000000..a555492ac91 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecord.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Record as part of {@link CO2EmissionRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record CO2EmissionRecord(@SerializedName("Minutes5UTC") Instant start, + @SerializedName("CO2Emission") BigDecimal emission) { + + public Instant end() { + return start.plus(5, ChronoUnit.MINUTES); + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecords.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecords.java new file mode 100644 index 00000000000..67f59c95968 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/api/dto/CO2EmissionRecords.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.energidataservice.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Received {@link CO2EmissionRecords} from Energi Data Service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public record CO2EmissionRecords(int total, String filters, int limit, String dataset, CO2EmissionRecord[] records) { +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java index 967322ad0db..2b2baba2c55 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/handler/EnergiDataServiceHandler.java @@ -47,9 +47,11 @@ import org.openhab.binding.energidataservice.internal.api.ChargeType; import org.openhab.binding.energidataservice.internal.api.ChargeTypeCode; import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory; +import org.openhab.binding.energidataservice.internal.api.Dataset; import org.openhab.binding.energidataservice.internal.api.DateQueryParameter; import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType; import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; +import org.openhab.binding.energidataservice.internal.api.dto.CO2EmissionRecord; import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecord; import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration; @@ -61,6 +63,7 @@ import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.CurrencyUnits; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -85,6 +88,9 @@ import org.slf4j.LoggerFactory; @NonNullByDefault public class EnergiDataServiceHandler extends BaseThingHandler { + private static final Duration emissionPrognosisJobInterval = Duration.ofMinutes(15); + private static final Duration emissionRealtimeJobInterval = Duration.ofMinutes(5); + private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class); private final TimeZoneProvider timeZoneProvider; private final ApiController apiController; @@ -92,7 +98,10 @@ public class EnergiDataServiceHandler extends BaseThingHandler { private EnergiDataServiceConfiguration config; private RetryStrategy retryPolicy = RetryPolicyFactory.initial(); - private @Nullable ScheduledFuture refreshFuture; + private boolean realtimeEmissionsFetchedFirstTime = false; + private @Nullable ScheduledFuture refreshPriceFuture; + private @Nullable ScheduledFuture refreshEmissionPrognosisFuture; + private @Nullable ScheduledFuture refreshEmissionRealtimeFuture; private @Nullable ScheduledFuture priceUpdateFuture; public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { @@ -111,8 +120,14 @@ public class EnergiDataServiceHandler extends BaseThingHandler { return; } - if (ELECTRICITY_CHANNELS.contains(channelUID.getId())) { + String channelId = channelUID.getId(); + if (ELECTRICITY_CHANNELS.contains(channelId)) { refreshElectricityPrices(); + } else if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelId)) { + rescheduleEmissionPrognosisJob(); + } else if (CHANNEL_CO2_EMISSION_REALTIME.equals(channelId)) { + realtimeEmissionsFetchedFirstTime = false; + rescheduleEmissionRealtimeJob(); } } @@ -140,15 +155,32 @@ public class EnergiDataServiceHandler extends BaseThingHandler { updateStatus(ThingStatus.UNKNOWN); - refreshFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS); + refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, 0, TimeUnit.SECONDS); + + if (isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) { + rescheduleEmissionPrognosisJob(); + } + if (isLinked(CHANNEL_CO2_EMISSION_REALTIME)) { + rescheduleEmissionRealtimeJob(); + } } @Override public void dispose() { - ScheduledFuture refreshFuture = this.refreshFuture; - if (refreshFuture != null) { - refreshFuture.cancel(true); - this.refreshFuture = null; + ScheduledFuture refreshPriceFuture = this.refreshPriceFuture; + if (refreshPriceFuture != null) { + refreshPriceFuture.cancel(true); + this.refreshPriceFuture = null; + } + ScheduledFuture refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture; + if (refreshEmissionPrognosisFuture != null) { + refreshEmissionPrognosisFuture.cancel(true); + this.refreshEmissionPrognosisFuture = null; + } + ScheduledFuture refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture; + if (refreshEmissionRealtimeFuture != null) { + refreshEmissionRealtimeFuture.cancel(true); + this.refreshEmissionRealtimeFuture = null; } ScheduledFuture priceUpdateFuture = this.priceUpdateFuture; if (priceUpdateFuture != null) { @@ -164,6 +196,30 @@ public class EnergiDataServiceHandler extends BaseThingHandler { return Set.of(EnergiDataServiceActions.class); } + @Override + public void channelUnlinked(ChannelUID channelUID) { + super.channelUnlinked(channelUID); + + if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelUID.getId()) && !isLinked(CHANNEL_CO2_EMISSION_PROGNOSIS)) { + logger.debug("No more items linked to channel '{}', stopping emission prognosis refresh job", + channelUID.getId()); + ScheduledFuture refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture; + if (refreshEmissionPrognosisFuture != null) { + refreshEmissionPrognosisFuture.cancel(true); + this.refreshEmissionPrognosisFuture = null; + } + } else if (CHANNEL_CO2_EMISSION_REALTIME.contains(channelUID.getId()) + && !isLinked(CHANNEL_CO2_EMISSION_REALTIME)) { + logger.debug("No more items linked to channel '{}', stopping realtime emission refresh job", + channelUID.getId()); + ScheduledFuture refreshEmissionRealtimeFuture = this.refreshEmissionRealtimeFuture; + if (refreshEmissionRealtimeFuture != null) { + refreshEmissionRealtimeFuture.cancel(true); + this.refreshEmissionRealtimeFuture = null; + } + } + } + private void refreshElectricityPrices() { RetryStrategy retryPolicy; try { @@ -208,7 +264,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler { return; } - rescheduleRefreshJob(retryPolicy); + reschedulePriceRefreshJob(retryPolicy); } private void downloadSpotPrices() throws InterruptedException, DataServiceException { @@ -299,6 +355,79 @@ public class EnergiDataServiceHandler extends BaseThingHandler { Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS))); } + private void refreshCo2EmissionPrognosis() { + try { + updateCo2Emissions(Dataset.CO2EmissionPrognosis, CHANNEL_CO2_EMISSION_PROGNOSIS, + DateQueryParameter.of(DateQueryParameterType.UTC_NOW, Duration.ofMinutes(-5))); + updateStatus(ThingStatus.ONLINE); + } catch (DataServiceException e) { + if (e.getHttpStatus() != 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + HttpStatus.getCode(e.getHttpStatus()).getMessage()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + } + if (e.getCause() != null) { + logger.debug("Error retrieving CO2 emission prognosis", e); + } + } catch (InterruptedException e) { + logger.debug("Emission prognosis refresh job interrupted"); + Thread.currentThread().interrupt(); + return; + } + } + + private void refreshCo2EmissionRealtime() { + try { + updateCo2Emissions(Dataset.CO2Emission, CHANNEL_CO2_EMISSION_REALTIME, + DateQueryParameter.of(DateQueryParameterType.UTC_NOW, + realtimeEmissionsFetchedFirstTime ? Duration.ofMinutes(-5) : Duration.ofHours(-24))); + realtimeEmissionsFetchedFirstTime = true; + updateStatus(ThingStatus.ONLINE); + } catch (DataServiceException e) { + if (e.getHttpStatus() != 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + HttpStatus.getCode(e.getHttpStatus()).getMessage()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + } + if (e.getCause() != null) { + logger.debug("Error retrieving CO2 realtime emissions", e); + } + } catch (InterruptedException e) { + logger.debug("Emission realtime refresh job interrupted"); + Thread.currentThread().interrupt(); + return; + } + } + + private void updateCo2Emissions(Dataset dataset, String channelId, DateQueryParameter dateQueryParameter) + throws InterruptedException, DataServiceException { + Map properties = editProperties(); + CO2EmissionRecord[] emissionRecords = apiController.getCo2Emissions(dataset, config.priceArea, + dateQueryParameter, properties); + updateProperties(properties); + + TimeSeries timeSeries = new TimeSeries(REPLACE); + Instant now = Instant.now(); + + if (dataset == Dataset.CO2Emission && emissionRecords.length > 0) { + // Records are sorted descending, first record is current. + updateState(channelId, new QuantityType<>(emissionRecords[0].emission(), Units.GRAM_PER_KILOWATT_HOUR)); + } + + for (CO2EmissionRecord emissionRecord : emissionRecords) { + State state = new QuantityType<>(emissionRecord.emission(), Units.GRAM_PER_KILOWATT_HOUR); + timeSeries.add(emissionRecord.start(), state); + + if (dataset == Dataset.CO2EmissionPrognosis && now.compareTo(emissionRecord.start()) >= 0 + && now.compareTo(emissionRecord.end()) < 0) { + updateState(channelId, state); + } + } + sendTimeSeries(channelId, timeSeries); + } + private void updatePrices() { cacheManager.cleanup(); @@ -472,19 +601,19 @@ public class EnergiDataServiceHandler extends BaseThingHandler { logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour); } - private void rescheduleRefreshJob(RetryStrategy retryPolicy) { + private void reschedulePriceRefreshJob(RetryStrategy retryPolicy) { // Preserve state of previous retry policy when configuration is the same. if (!retryPolicy.equals(this.retryPolicy)) { this.retryPolicy = retryPolicy; } - ScheduledFuture refreshJob = this.refreshFuture; + ScheduledFuture refreshJob = this.refreshPriceFuture; long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds(); Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh); - this.refreshFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh, + this.refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh, TimeUnit.SECONDS); - logger.debug("Refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh); + logger.debug("Price refresh job rescheduled in {} seconds: {}", secondsUntilNextRefresh, timeOfNextRefresh); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT); updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone()) .truncatedTo(ChronoUnit.SECONDS).format(formatter)); @@ -493,4 +622,28 @@ public class EnergiDataServiceHandler extends BaseThingHandler { refreshJob.cancel(true); } } + + private void rescheduleEmissionPrognosisJob() { + logger.debug("Scheduling emission prognosis refresh job now and every {}", emissionPrognosisJobInterval); + + ScheduledFuture refreshEmissionPrognosisFuture = this.refreshEmissionPrognosisFuture; + if (refreshEmissionPrognosisFuture != null) { + refreshEmissionPrognosisFuture.cancel(true); + } + + this.refreshEmissionPrognosisFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionPrognosis, 0, + emissionPrognosisJobInterval.toSeconds(), TimeUnit.SECONDS); + } + + private void rescheduleEmissionRealtimeJob() { + logger.debug("Scheduling emission realtime refresh job now and every {}", emissionRealtimeJobInterval); + + ScheduledFuture refreshEmissionFuture = this.refreshEmissionRealtimeFuture; + if (refreshEmissionFuture != null) { + refreshEmissionFuture.cancel(true); + } + + this.refreshEmissionRealtimeFuture = scheduler.scheduleWithFixedDelay(this::refreshCo2EmissionRealtime, 0, + emissionRealtimeJobInterval.toSeconds(), TimeUnit.SECONDS); + } } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties index f54cbd7cc0c..516b6752fe6 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/i18n/energidataservice.properties @@ -58,6 +58,10 @@ thing-type.config.energidataservice.service.reducedElectricityTax.description = channel-group-type.energidataservice.electricity.label = Electricity channel-group-type.energidataservice.electricity.description = Channels related to electricity +channel-group-type.energidataservice.electricity.channel.co2-emission-prognosis.label = CO₂ Emission Prognosis +channel-group-type.energidataservice.electricity.channel.co2-emission-prognosis.description = Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh. +channel-group-type.energidataservice.electricity.channel.co2-emission-realtime.label = CO₂ Emission Realtime +channel-group-type.energidataservice.electricity.channel.co2-emission-realtime.description = Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh. channel-group-type.energidataservice.electricity.channel.electricity-tax.label = Electricity Tax channel-group-type.energidataservice.electricity.channel.electricity-tax.description = Electricity tax in DKK per kWh. channel-group-type.energidataservice.electricity.channel.grid-tariff.label = Grid Tariff @@ -73,6 +77,8 @@ channel-group-type.energidataservice.electricity.channel.transmission-grid-tarif # channel types +channel-type.energidataservice.co2-emission.label = CO₂ Emission +channel-type.energidataservice.co2-emission.description = CO₂ emission in g/kWh. channel-type.energidataservice.datahub-price.label = Datahub Price channel-type.energidataservice.datahub-price.description = Datahub price. channel-type.energidataservice.spot-price.label = Spot Price diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml index 44cef47c1a0..add4ee51c21 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-groups.xml @@ -32,6 +32,14 @@ Reduced electricity tax in DKK per kWh. For electric heating customers only. + + + Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh. + + + + Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh. + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml index fe055d40ea7..1509d177dbb 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/channel-types.xml @@ -21,4 +21,12 @@ + + Number:EmissionIntensity + + CO₂ emission in g/kWh. + Smoke + + + diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml index bda2a09c9a8..fca5f24f6a9 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/thing/thing-service.xml @@ -14,7 +14,7 @@ - 4 + 5 diff --git a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/update/instructions.xml index 1d71298194b..1366e1898b5 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/update/instructions.xml +++ b/bundles/org.openhab.binding.energidataservice/src/main/resources/OH-INF/update/instructions.xml @@ -65,6 +65,19 @@ + + + energidataservice:co2-emission + + Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh. + + + energidataservice:co2-emission + + Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh. + + +