Add CO2 emission channels (#16330)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-03-15 12:26:42 +01:00 committed by Ciprian Pascu
parent 55c84385ab
commit a26a84c958
12 changed files with 399 additions and 61 deletions

View File

@ -47,22 +47,24 @@ It will not impact channels, see [Electricity Tax](#electricity-tax) for further
### Channel Group `electricity` ### Channel Group `electricity`
| Channel | Type | Description | Advanced | | Channel | Type | Description |
|--------------------------|--------------------|--------------------------------------------------------------------------------|----------| |--------------------------|--------------------------|----------------------------------------------------------------------------------------|
| spot-price | Number:EnergyPrice | Spot price in DKK or EUR per kWh | no | | 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 | no | | 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 | no | | system-tariff | Number:EnergyPrice | System tariff in DKK per kWh |
| transmission-grid-tariff | Number:EnergyPrice | Transmission grid tariff in DKK per kWh | no | | transmission-grid-tariff | Number:EnergyPrice | Transmission grid tariff in DKK per kWh |
| electricity-tax | Number:EnergyPrice | Electricity tax in DKK per kWh | no | | 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 | no | | 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. _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. Instead, create a group item with `SUM` as aggregate function and add the individual price items as children.
This has the following advantages: This has the following advantages:
- Full customization possible: Freely choose the channels which should be included in the total. - Full customization possible: Freely choose the channels which should be included in the total (even between different bindings).
- 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 (and currency conversions are performed outside the binding).
- Spot price can be configured in EUR while tariffs are in DKK. - 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. 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. 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. 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. 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
Thing actions can be used to perform calculations as well as import prices directly into rules without relying on persistence. Thing actions can be used to perform calculations as well as import prices directly into rules without relying on persistence.

View File

@ -38,8 +38,11 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.energidataservice.internal.api.ChargeType; import org.openhab.binding.energidataservice.internal.api.ChargeType;
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; 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.DateQueryParameter;
import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; 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.DatahubPricelistRecord;
import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords; import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; 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 ENDPOINT = "https://api.energidataservice.dk/";
private static final String DATASET_PATH = "dataset/"; 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_PRICE_AREA = "PriceArea";
private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType"; private static final String FILTER_KEY_CHARGE_TYPE = "ChargeType";
private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode"; private static final String FILTER_KEY_CHARGE_TYPE_CODE = "ChargeTypeCode";
@ -111,7 +111,7 @@ public class ApiController {
throw new IllegalArgumentException("Invalid currency " + currency.getCurrencyCode()); 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) // .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) //
.param("start", start.toString()) // .param("start", start.toString()) //
.param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") // .param("filter", "{\"" + FILTER_KEY_PRICE_AREA + "\":\"" + priceArea + "\"}") //
@ -119,23 +119,8 @@ public class ApiController {
.agent(userAgent) // .agent(userAgent) //
.method(HttpMethod.GET); .method(HttpMethod.GET);
logger.trace("GET request for {}", request.getURI());
try { try {
ContentResponse response = request.send(); String responseContent = sendRequest(request, properties);
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);
ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class); ElspotpriceRecords records = gson.fromJson(responseContent, ElspotpriceRecords.class);
if (records == null) { if (records == null) {
throw new DataServiceException("Error parsing response"); throw new DataServiceException("Error parsing response");
@ -153,6 +138,27 @@ public class ApiController {
} }
} }
private String sendRequest(Request request, Map<String, String> 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<String, String> properties) { private void updatePropertiesFromResponse(ContentResponse response, Map<String, String> properties) {
HttpFields headers = response.getHeaders(); HttpFields headers = response.getHeaders();
String remainingCalls = headers.get(HEADER_REMAINING_CALLS); String remainingCalls = headers.get(HEADER_REMAINING_CALLS);
@ -200,7 +206,7 @@ public class ApiController {
filterMap.put(FILTER_KEY_NOTE, notes); 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) // .timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) //
.param("filter", mapToFilter(filterMap)) // .param("filter", mapToFilter(filterMap)) //
.param("columns", columns) // .param("columns", columns) //
@ -212,23 +218,8 @@ public class ApiController {
request = request.param("start", dateQueryParameter.toString()); request = request.param("start", dateQueryParameter.toString());
} }
logger.trace("GET request for {}", request.getURI());
try { try {
ContentResponse response = request.send(); String responseContent = sendRequest(request, properties);
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);
DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class); DatahubPricelistRecords records = gson.fromJson(responseContent, DatahubPricelistRecords.class);
if (records == null) { if (records == null) {
throw new DataServiceException("Error parsing response"); throw new DataServiceException("Error parsing response");
@ -255,4 +246,48 @@ public class ApiController {
e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]") e -> "\"" + e.getKey() + "\":[\"" + e.getValue().stream().collect(Collectors.joining("\",\"")) + "\"]")
.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<String, String> 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);
}
}
} }

View File

@ -52,6 +52,10 @@ public class EnergiDataServiceBindingConstants {
+ ChannelUID.CHANNEL_GROUP_SEPARATOR + "reduced-electricity-tax"; + ChannelUID.CHANNEL_GROUP_SEPARATOR + "reduced-electricity-tax";
public static final String CHANNEL_TRANSMISSION_GRID_TARIFF = CHANNEL_GROUP_ELECTRICITY public static final String CHANNEL_TRANSMISSION_GRID_TARIFF = CHANNEL_GROUP_ELECTRICITY
+ ChannelUID.CHANNEL_GROUP_SEPARATOR + "transmission-grid-tariff"; + 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<String> ELECTRICITY_CHANNELS = Set.of(CHANNEL_SPOT_PRICE, CHANNEL_GRID_TARIFF, public static final Set<String> ELECTRICITY_CHANNELS = Set.of(CHANNEL_SPOT_PRICE, CHANNEL_GRID_TARIFF,
CHANNEL_SYSTEM_TARIFF, CHANNEL_TRANSMISSION_GRID_TARIFF, CHANNEL_ELECTRICITY_TAX, CHANNEL_SYSTEM_TARIFF, CHANNEL_TRANSMISSION_GRID_TARIFF, CHANNEL_ELECTRICITY_TAX,

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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) {
}

View File

@ -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.ChargeTypeCode;
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter; import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilter;
import org.openhab.binding.energidataservice.internal.api.DatahubTariffFilterFactory; 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.DateQueryParameter;
import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType; import org.openhab.binding.energidataservice.internal.api.DateQueryParameterType;
import org.openhab.binding.energidataservice.internal.api.GlobalLocationNumber; 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.DatahubPricelistRecord;
import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord; import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecord;
import org.openhab.binding.energidataservice.internal.config.DatahubPriceConfiguration; 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.DecimalType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.CurrencyUnits; 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.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -85,6 +88,9 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class EnergiDataServiceHandler extends BaseThingHandler { 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 Logger logger = LoggerFactory.getLogger(EnergiDataServiceHandler.class);
private final TimeZoneProvider timeZoneProvider; private final TimeZoneProvider timeZoneProvider;
private final ApiController apiController; private final ApiController apiController;
@ -92,7 +98,10 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
private EnergiDataServiceConfiguration config; private EnergiDataServiceConfiguration config;
private RetryStrategy retryPolicy = RetryPolicyFactory.initial(); 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; private @Nullable ScheduledFuture<?> priceUpdateFuture;
public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { public EnergiDataServiceHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
@ -111,8 +120,14 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
return; return;
} }
if (ELECTRICITY_CHANNELS.contains(channelUID.getId())) { String channelId = channelUID.getId();
if (ELECTRICITY_CHANNELS.contains(channelId)) {
refreshElectricityPrices(); 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); 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 @Override
public void dispose() { public void dispose() {
ScheduledFuture<?> refreshFuture = this.refreshFuture; ScheduledFuture<?> refreshPriceFuture = this.refreshPriceFuture;
if (refreshFuture != null) { if (refreshPriceFuture != null) {
refreshFuture.cancel(true); refreshPriceFuture.cancel(true);
this.refreshFuture = null; 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; ScheduledFuture<?> priceUpdateFuture = this.priceUpdateFuture;
if (priceUpdateFuture != null) { if (priceUpdateFuture != null) {
@ -164,6 +196,30 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
return Set.of(EnergiDataServiceActions.class); 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() { private void refreshElectricityPrices() {
RetryStrategy retryPolicy; RetryStrategy retryPolicy;
try { try {
@ -208,7 +264,7 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
return; return;
} }
rescheduleRefreshJob(retryPolicy); reschedulePriceRefreshJob(retryPolicy);
} }
private void downloadSpotPrices() throws InterruptedException, DataServiceException { private void downloadSpotPrices() throws InterruptedException, DataServiceException {
@ -299,6 +355,79 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
Duration.ofHours(-CacheManager.NUMBER_OF_HISTORIC_HOURS))); 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<String, String> 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() { private void updatePrices() {
cacheManager.cleanup(); cacheManager.cleanup();
@ -472,19 +601,19 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
logger.debug("Price update job rescheduled in {} milliseconds", millisUntilNextClockHour); 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. // Preserve state of previous retry policy when configuration is the same.
if (!retryPolicy.equals(this.retryPolicy)) { if (!retryPolicy.equals(this.retryPolicy)) {
this.retryPolicy = retryPolicy; this.retryPolicy = retryPolicy;
} }
ScheduledFuture<?> refreshJob = this.refreshFuture; ScheduledFuture<?> refreshJob = this.refreshPriceFuture;
long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds(); long secondsUntilNextRefresh = this.retryPolicy.getDuration().getSeconds();
Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh); Instant timeOfNextRefresh = Instant.now().plusSeconds(secondsUntilNextRefresh);
this.refreshFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh, this.refreshPriceFuture = scheduler.schedule(this::refreshElectricityPrices, secondsUntilNextRefresh,
TimeUnit.SECONDS); 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); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PROPERTY_DATETIME_FORMAT);
updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone()) updateProperty(PROPERTY_NEXT_CALL, LocalDateTime.ofInstant(timeOfNextRefresh, timeZoneProvider.getTimeZone())
.truncatedTo(ChronoUnit.SECONDS).format(formatter)); .truncatedTo(ChronoUnit.SECONDS).format(formatter));
@ -493,4 +622,28 @@ public class EnergiDataServiceHandler extends BaseThingHandler {
refreshJob.cancel(true); 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);
}
} }

View File

@ -58,6 +58,10 @@ thing-type.config.energidataservice.service.reducedElectricityTax.description =
channel-group-type.energidataservice.electricity.label = Electricity channel-group-type.energidataservice.electricity.label = Electricity
channel-group-type.energidataservice.electricity.description = Channels related to 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.label = Electricity Tax
channel-group-type.energidataservice.electricity.channel.electricity-tax.description = Electricity tax in DKK per kWh. 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 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 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.label = Datahub Price
channel-type.energidataservice.datahub-price.description = Datahub price. channel-type.energidataservice.datahub-price.description = Datahub price.
channel-type.energidataservice.spot-price.label = Spot Price channel-type.energidataservice.spot-price.label = Spot Price

View File

@ -32,6 +32,14 @@
<label>Reduced Electricity Tax</label> <label>Reduced Electricity Tax</label>
<description>Reduced electricity tax in DKK per kWh. For electric heating customers only.</description> <description>Reduced electricity tax in DKK per kWh. For electric heating customers only.</description>
</channel> </channel>
<channel id="co2-emission-prognosis" typeId="co2-emission">
<label>CO₂ Emission Prognosis</label>
<description>Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh.</description>
</channel>
<channel id="co2-emission-realtime" typeId="co2-emission">
<label>CO₂ Emission Realtime</label>
<description>Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh.</description>
</channel>
</channels> </channels>
</channel-group-type> </channel-group-type>

View File

@ -21,4 +21,12 @@
<config-description-ref uri="channel-type:energidataservice:datahub-price"/> <config-description-ref uri="channel-type:energidataservice:datahub-price"/>
</channel-type> </channel-type>
<channel-type id="co2-emission">
<item-type>Number:EmissionIntensity</item-type>
<label>CO₂ Emission</label>
<description>CO₂ emission in g/kWh.</description>
<category>Smoke</category>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -14,7 +14,7 @@
</channel-groups> </channel-groups>
<properties> <properties>
<property name="thingTypeVersion">4</property> <property name="thingTypeVersion">5</property>
</properties> </properties>
<config-description-ref uri="thing-type:energidataservice:service"/> <config-description-ref uri="thing-type:energidataservice:service"/>

View File

@ -65,6 +65,19 @@
<remove-channel id="hourly-prices" groupIds="electricity"/> <remove-channel id="hourly-prices" groupIds="electricity"/>
</instruction-set> </instruction-set>
<instruction-set targetVersion="5">
<add-channel id="co2-emission-prognosis" groupIds="electricity">
<type>energidataservice:co2-emission</type>
<label>CO₂ Emission Prognosis</label>
<description>Estimated prognosis for CO₂ emission following the day-ahead market in g/kWh.</description>
</add-channel>
<add-channel id="co2-emission-realtime" groupIds="electricity">
<type>energidataservice:co2-emission</type>
<label>CO₂ Emission Realtime</label>
<description>Near up-to-date history for CO₂ emission from electricity consumed in Denmark in g/kWh.</description>
</add-channel>
</instruction-set>
</thing-type> </thing-type>
</update:update-descriptions> </update:update-descriptions>