From 99892c56eb7f3b02339174e65f8b389e672d298c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Melhus?= <5099412+jmelhus@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:32:23 +0200 Subject: [PATCH] [entsoe] Initial contribution (#17416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jørgen Melhus --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.entsoe/NOTICE | 13 + bundles/org.openhab.binding.entsoe/README.md | 85 ++++++ bundles/org.openhab.binding.entsoe/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../internal/EntsoeBindingConstants.java | 44 +++ .../entsoe/internal/EntsoeConfiguration.java | 30 ++ .../entsoe/internal/EntsoeHandler.java | 273 ++++++++++++++++++ .../entsoe/internal/EntsoeHandlerFactory.java | 51 ++++ .../entsoe/internal/client/Client.java | 175 +++++++++++ .../entsoe/internal/client/Request.java | 62 ++++ .../entsoe/internal/client/SpotPrice.java | 93 ++++++ .../EntsoeConfigurationException.java | 33 +++ .../exception/EntsoeResponseException.java | 37 +++ .../exception/EntsoeResponseMapException.java | 33 +++ .../src/main/resources/OH-INF/addon/addon.xml | 13 + .../resources/OH-INF/i18n/entsoe.properties | 142 +++++++++ .../resources/OH-INF/thing/thing-types.xml | 194 +++++++++++++ bundles/pom.xml | 1 + 20 files changed, 1311 insertions(+) create mode 100644 bundles/org.openhab.binding.entsoe/NOTICE create mode 100644 bundles/org.openhab.binding.entsoe/README.md create mode 100644 bundles/org.openhab.binding.entsoe/pom.xml create mode 100644 bundles/org.openhab.binding.entsoe/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeBindingConstants.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeConfiguration.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/SpotPrice.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeConfigurationException.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseException.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseMapException.java create mode 100644 bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/i18n/entsoe.properties create mode 100644 bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index f2eb395a8a3..9da5cbb79ee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,6 +104,7 @@ /bundles/org.openhab.binding.enigma2/ @gdolfen /bundles/org.openhab.binding.enocean/ @fruggy83 /bundles/org.openhab.binding.enphase/ @Hilbrand +/bundles/org.openhab.binding.entsoe/ @jmelhus /bundles/org.openhab.binding.enturno/ @klocsson /bundles/org.openhab.binding.ephemeris/ @clinique /bundles/org.openhab.binding.epsonprojector/ @mlobstein diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 0604b3a3956..3bd18dbc0ba 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -511,6 +511,11 @@ org.openhab.binding.enphase ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.entsoe + ${project.version} + org.openhab.addons.bundles org.openhab.binding.enturno diff --git a/bundles/org.openhab.binding.entsoe/NOTICE b/bundles/org.openhab.binding.entsoe/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.entsoe/README.md b/bundles/org.openhab.binding.entsoe/README.md new file mode 100644 index 00000000000..e3ba543abe1 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/README.md @@ -0,0 +1,85 @@ +# ENTSO-E Binding + +This binding fetches day-ahead energy spot prices from ENTSO-E, the European Network of Transmission System Operators for Electricity. + +Users can select a specific area to retrieve the relevant energy prices. +This binding helps users monitor and manage their energy consumption based on real-time pricing data. +It is recommended to use this binding together with a currency provider (e.g. [Freecurrency binding](https://www.openhab.org/addons/bindings/freecurrency/)) for exchanging euro spot prices to local currency. + +## Supported Things + +- `day-ahead`: This is the main and single Thing of the binding. + +## Thing Configuration + +To access the ENTSO-E Transparency Platform API, users need a **security token** for authentication and authorization. +This token ensures secure access to the platform's data and services. +For detailed instructions on obtaining this token, you can refer to the [ENTSO-E API Guide 2. Authentication and Authorisation](https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_authentication_and_authorisation). + +Mandatory parameters of the Thing are security token and area. +Optional parameters are historic days, resolution, availability hour for day ahead spot prices and request timeout. + +### `entsoe` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-------------------------------|-------------------|---------------------------------------------------------------------------|-----------|----------|----------| +| securityToken | text | Security token to fetch from ENTSO-E | N/A | yes | no | +| area | text | Area | N/A | yes | no | +| historicDays | integer | Historic days to get prices from (will use exchange rate as of today) | 0 | no | no | +| resolution | text | Data resolution | PT60M | no | no | +| spotPricesAvailableCetHour | integer | Which CET hour binding assumes new spot prices for next day is available | 13 | no | yes | +| requestTimeout | integer | Request timeout in seconds | 30 | no | yes | + +## Channels + +Binding has one channel. + +spot-price which are the values fetched from ENTSO-E and persisted in openHAB as time series. +The price is per kWh at your selected base currency. + +| Channel | Type | Read/Write | Description | +|--------------------------|-----------------------|------------|-------------------------------------------| +| spot-price | Number:EnergyPrice | R | Spot prices | + +### Thing Configuration + +```java +Thing entsoe:day-ahead:eda "Entsoe Day Ahead" [ securityToken="your-security-token", area="10YNO-3--------J", historicDays=14 ] +``` + +### Item Configuration + +```java +Number:EnergyPrice energySpotPrice "Current Spot Price" { channel="entsoe:day-ahead:eda:spot-price" } +``` + +#### Value-Added Tax + +VAT is not included in any of the prices. +To include VAT for items linked to the `Number:EnergyPrice` channel, the [VAT profile](https://www.openhab.org/addons/transformations/vat/) can be used. +This must be installed separately. +Once installed, simply select "Value-Added Tax" as Profile when linking an item. + +#### 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. +Read more about how to in this similar binding [Energi Data Service](https://www.openhab.org/addons/bindings/energidataservice/#total-price) + +### Trigger Channels + +Channel `prices-received` is triggered when new prices are available. + +### Examples + +examples.rules + +```java +rule "Spot prices received" +when + Channel "entsoe:day-ahead:eda:prices-received" triggered +then + // Do something within rule + logInfo("ENTSO-E Rule", "ENTSO-E channel triggered, new spot prices available") +end +``` diff --git a/bundles/org.openhab.binding.entsoe/pom.xml b/bundles/org.openhab.binding.entsoe/pom.xml new file mode 100644 index 00000000000..baf37c2bf9b --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.entsoe + + openHAB Add-ons :: Bundles :: ENTSO-E Binding + + diff --git a/bundles/org.openhab.binding.entsoe/src/main/feature/feature.xml b/bundles/org.openhab.binding.entsoe/src/main/feature/feature.xml new file mode 100644 index 00000000000..701a921c9a3 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.entsoe/${project.version} + + diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeBindingConstants.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeBindingConstants.java new file mode 100644 index 00000000000..9356668e3f1 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeBindingConstants.java @@ -0,0 +1,44 @@ +/** + * 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.entsoe.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.TimeSeries.Policy; + +/** + * The {@link EntsoeBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jørgen Melhus - Initial contribution + * @author Miika Jukka - Initial contribution + */ +@NonNullByDefault +public class EntsoeBindingConstants { + + private static final String BINDING_ID = "entsoe"; + + public static final Policy TIMESERIES_POLICY = Policy.REPLACE; + + public static final String CHANNEL_SPOT_PRICE = "spot-price"; + + public static final String CHANNEL_TRIGGER_PRICES_RECEIVED = "prices-received"; + + // Thing Type UIDs + public static final ThingTypeUID THING_TYPE_DAY_AHEAD = new ThingTypeUID(BINDING_ID, "day-ahead"); + + // List of all Thing Type UIDs + public static final Set SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_DAY_AHEAD); +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeConfiguration.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeConfiguration.java new file mode 100644 index 00000000000..3b5cd45afc4 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeConfiguration.java @@ -0,0 +1,30 @@ +/** + * 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.entsoe.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EntsoeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jørgen Melhus - Initial contribution + */ +@NonNullByDefault +public class EntsoeConfiguration { + public String securityToken = ""; + public String area = ""; + public int spotPricesAvailableCetHour = 13; + public int historicDays = 1; + public int requestTimeout = 30; + public String resolution = "PT60M"; +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java new file mode 100644 index 00000000000..78ba5c27068 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java @@ -0,0 +1,273 @@ +/** + * 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.entsoe.internal; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.entsoe.internal.client.Client; +import org.openhab.binding.entsoe.internal.client.Request; +import org.openhab.binding.entsoe.internal.client.SpotPrice; +import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException; +import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException; +import org.openhab.binding.entsoe.internal.exception.EntsoeResponseMapException; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.TimeSeries; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EntsoeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jørgen Melhus - Initial contribution + */ +@NonNullByDefault +public class EntsoeHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EntsoeHandler.class); + + private EntsoeConfiguration config; + + private @Nullable ScheduledFuture refreshJob; + + private Map entsoeTimeSeries = new LinkedHashMap<>(); + + private final ZoneId cetZoneId = ZoneId.of("CET"); + + private ZonedDateTime lastDayAheadReceived = ZonedDateTime.of(LocalDateTime.MIN, cetZoneId); + + private int historicDaysInitially = 0; + + public EntsoeHandler(Thing thing) { + super(thing); + config = new EntsoeConfiguration(); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + logger.trace("channelLinked(channelUID:{})", channelUID.getAsString()); + String channelID = channelUID.getId(); + + if (EntsoeBindingConstants.CHANNEL_SPOT_PRICE.equals(channelID)) { + if (entsoeTimeSeries.isEmpty()) { + refreshPrices(); + } + updateCurrentState(EntsoeBindingConstants.CHANNEL_SPOT_PRICE); + } + } + + @Override + public void dispose() { + entsoeTimeSeries.clear(); + ScheduledFuture refreshJob = this.refreshJob; + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("handleCommand(channelUID:{}, command:{})", channelUID.getAsString(), command.toFullString()); + + if (command instanceof RefreshType) { + fetchNewPrices(); + } + } + + @Override + public void initialize() { + config = getConfigAs(EntsoeConfiguration.class); + lastDayAheadReceived = ZonedDateTime.of(LocalDateTime.MIN, cetZoneId); + + if (historicDaysInitially == 0) { + historicDaysInitially = config.historicDays; + } + + if (isLinked(EntsoeBindingConstants.CHANNEL_SPOT_PRICE)) { + updateStatus(ThingStatus.UNKNOWN); + } else { + updateStatus(ThingStatus.ONLINE); + } + + refreshJob = scheduler.schedule(this::refreshPrices, 0, TimeUnit.SECONDS); + } + + private ZonedDateTime currentCetTime() { + return ZonedDateTime.now(cetZoneId); + } + + private ZonedDateTime currentCetTimeWholeHours() { + return currentCetTime().truncatedTo(ChronoUnit.HOURS); + } + + private long getMillisToNextStateUpdate() { + Instant now = Instant.now(); + Instant nextHour = now.truncatedTo(ChronoUnit.HOURS).plus(1, ChronoUnit.HOURS); + long millis = Duration.between(now, nextHour).toMillis(); + try { + Instant nextInstant = getNextSpotPrice().getInstant(); + millis = Duration.between(now, nextInstant).toMillis(); + } catch (EntsoeResponseMapException e) { + logger.warn("Using millis to next whole hour"); + } + return millis + 1; + } + + private SpotPrice getCurrentSpotPrice() throws EntsoeResponseMapException { + Duration duration = Duration.parse(config.resolution); + Instant now = Instant.now(); + + for (Map.Entry entry : entsoeTimeSeries.entrySet()) { + Instant entryStart = entry.getKey(); + Instant entryEnd = entryStart.plus(duration); + + if (!now.isBefore(entryStart) && now.isBefore(entryEnd)) { + return entry.getValue(); + } + } + throw new EntsoeResponseMapException("Could not get current SpotPrice"); + } + + private SpotPrice getNextSpotPrice() throws EntsoeResponseMapException { + Instant now = Instant.now(); + + for (Map.Entry entry : entsoeTimeSeries.entrySet()) { + Instant entryStart = entry.getKey(); + + if (entryStart.isAfter(now)) { + return entry.getValue(); + } + } + throw new EntsoeResponseMapException("Could not get next SpotPrice"); + } + + private boolean needToFetchHistoricDays() { + return needToFetchHistoricDays(false); + } + + private boolean needToFetchHistoricDays(boolean updateHistoricDaysInitially) { + boolean needToFetch = false; + if (historicDaysInitially < config.historicDays) { + logger.debug("Need to fetch historic data. Historicdays was changed to a greater number: {}", + config.historicDays); + needToFetch = true; + } + + if (updateHistoricDaysInitially && historicDaysInitially != config.historicDays) { + historicDaysInitially = config.historicDays; + } + + return needToFetch; + } + + private void refreshPrices() { + if (!isLinked(EntsoeBindingConstants.CHANNEL_SPOT_PRICE)) { + logger.debug("Channel {} is not linked, skipping channel update", + EntsoeBindingConstants.CHANNEL_SPOT_PRICE); + return; + } + + if (entsoeTimeSeries.isEmpty()) { + fetchNewPrices(); + return; + } + + boolean needsInitialUpdate = lastDayAheadReceived.equals(ZonedDateTime.of(LocalDateTime.MIN, cetZoneId)) + || needToFetchHistoricDays(true); + Instant nextDayInstant = ZonedDateTime.now(cetZoneId).plusDays(1).with(LocalTime.MIDNIGHT).toInstant(); + boolean hasNextDayValue = entsoeTimeSeries.containsKey(nextDayInstant); + boolean readyForNextDayValue = currentCetTime() + .isAfter(currentCetTimeWholeHours().withHour(config.spotPricesAvailableCetHour)); + + if (needsInitialUpdate || (!hasNextDayValue && readyForNextDayValue)) { + fetchNewPrices(); + } else { + updateCurrentState(EntsoeBindingConstants.CHANNEL_SPOT_PRICE); + schedule(true); + } + } + + private void fetchNewPrices() { + logger.trace("Fetching new prices"); + + Instant startUtc = ZonedDateTime.now(cetZoneId) + .minusDays(needToFetchHistoricDays() ? config.historicDays - 1 : 0).with(LocalTime.MIDNIGHT) + .toInstant(); + Instant endUtc = ZonedDateTime.now(cetZoneId).plusDays(2).with(LocalTime.MIDNIGHT).toInstant(); + + Request request = new Request(config.securityToken, config.area, startUtc, endUtc); + Client client = new Client(); + boolean success = false; + + try { + entsoeTimeSeries = client.doGetRequest(request, config.requestTimeout * 1000, config.resolution); + + TimeSeries baseTimeSeries = new TimeSeries(EntsoeBindingConstants.TIMESERIES_POLICY); + for (Map.Entry entry : entsoeTimeSeries.entrySet()) { + baseTimeSeries.add(entry.getValue().getInstant(), entry.getValue().getState(Units.KILOWATT_HOUR)); + } + + updateStatus(ThingStatus.ONLINE); + lastDayAheadReceived = currentCetTime(); + sendTimeSeries(EntsoeBindingConstants.CHANNEL_SPOT_PRICE, baseTimeSeries); + updateCurrentState(EntsoeBindingConstants.CHANNEL_SPOT_PRICE); + triggerChannel(EntsoeBindingConstants.CHANNEL_TRIGGER_PRICES_RECEIVED); + success = true; + } catch (EntsoeResponseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (EntsoeConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } finally { + schedule(success); + } + } + + private void schedule(boolean success) { + if (!success) { + logger.trace("schedule(success:{})", success); + refreshJob = scheduler.schedule(this::refreshPrices, 5, TimeUnit.MINUTES); + } else { + refreshJob = scheduler.schedule(this::refreshPrices, getMillisToNextStateUpdate(), TimeUnit.MILLISECONDS); + } + } + + private void updateCurrentState(String channelID) { + logger.trace("Updating current state"); + try { + updateState(channelID, getCurrentSpotPrice().getState(Units.KILOWATT_HOUR)); + } catch (EntsoeConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java new file mode 100644 index 00000000000..8fa2febdaeb --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java @@ -0,0 +1,51 @@ +/** + * 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.entsoe.internal; + +import static org.openhab.binding.entsoe.internal.EntsoeBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link EntsoeHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jørgen Melhus - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.entsoe", service = ThingHandlerFactory.class) +public class EntsoeHandlerFactory extends BaseThingHandlerFactory { + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_DAY_AHEAD.equals(thingTypeUID)) { + return new EntsoeHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java new file mode 100644 index 00000000000..4384d69ff5c --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java @@ -0,0 +1,175 @@ +/** + * 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.entsoe.internal.client; + +import java.io.IOException; +import java.io.StringReader; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException; +import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException; +import org.openhab.core.io.net.http.HttpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * @author Miika Jukka - Initial contribution + * @author Jørgen Melhus - Contribution + */ +@NonNullByDefault +public class Client { + private final Logger logger = LoggerFactory.getLogger(Client.class); + + private DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + public Map doGetRequest(Request request, int timeout, String configResolution) + throws EntsoeResponseException, EntsoeConfigurationException { + try { + logger.debug("Sending GET request with parameters: {}", request); + String url = request.toUrl(); + String responseText = HttpUtil.executeUrl("GET", url, timeout); + if (responseText == null) { + throw new EntsoeResponseException("Request failed"); + } + logger.trace("Response: {}", responseText); + return parseXmlResponse(responseText, configResolution); + } catch (IOException e) { + String message = e.getMessage(); + if (message != null && message.contains("Authentication challenge without WWW-Authenticate header")) { + throw new EntsoeConfigurationException("Authentication failed. Please check your security token"); + } + throw new EntsoeResponseException(e); + } catch (ParserConfigurationException | SAXException e) { + throw new EntsoeResponseException(e); + } + } + + private Map parseXmlResponse(String responseText, String configResolution) + throws ParserConfigurationException, SAXException, IOException, EntsoeResponseException, + EntsoeConfigurationException { + Map responseMap = new LinkedHashMap<>(); + + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(new InputSource(new StringReader(responseText))); + document.getDocumentElement().normalize(); + + // Check for rejection + if (document.getDocumentElement().getNodeName().equals("Acknowledgement_MarketDocument")) { + NodeList reasonOfRejection = document.getElementsByTagName("Reason"); + Node reasonNode = reasonOfRejection.item(0); + Element reasonElement = (Element) reasonNode; + String reasonCode = reasonElement.getElementsByTagName("code").item(0).getTextContent(); + String reasonText = reasonElement.getElementsByTagName("text").item(0).getTextContent(); + throw new EntsoeResponseException( + String.format("Request failed with API response: Code %s, Text %s", reasonCode, reasonText)); + } + + // Get all "timeSeries" nodes from document + NodeList listOfTimeSeries = document.getElementsByTagName("TimeSeries"); + boolean resolutionFound = false; + for (int i = 0; i < listOfTimeSeries.getLength(); i++) { + Node timeSeriesNode = listOfTimeSeries.item(i); + if (timeSeriesNode.getNodeType() == Node.ELEMENT_NODE) { + Element timeSeriesElement = (Element) timeSeriesNode; + + String currency = timeSeriesElement.getElementsByTagName("currency_Unit.name").item(0).getTextContent(); + String measureUnit = timeSeriesElement.getElementsByTagName("price_Measure_Unit.name").item(0) + .getTextContent(); + + NodeList listOfPeriod = timeSeriesElement.getElementsByTagName("Period"); + Node periodNode = listOfPeriod.item(0); + Element periodElement = (Element) periodNode; + + NodeList listOfTimeInterval = periodElement.getElementsByTagName("timeInterval"); + Node startTimeNode = listOfTimeInterval.item(0); + Element startTimeElement = (Element) startTimeNode; + String startTime = startTimeElement.getElementsByTagName("start").item(0).getTextContent(); + Instant startTimeInstant = ZonedDateTime.parse(startTime, DateTimeFormatter.ISO_ZONED_DATE_TIME) + .toInstant(); + Node endTimeNode = listOfTimeInterval.item(0); + Element endTimeElement = (Element) endTimeNode; + String endTime = endTimeElement.getElementsByTagName("end").item(0).getTextContent(); + Instant endTimeInstant = ZonedDateTime.parse(endTime, DateTimeFormatter.ISO_ZONED_DATE_TIME) + .toInstant(); + + String resolution = periodElement.getElementsByTagName("resolution").item(0).getTextContent(); + Duration durationResolution = Duration.parse(resolution); + Duration durationTotal = Duration.between(startTimeInstant, endTimeInstant); + int numberOfDurations = Math.round(durationTotal.toMinutes() / durationResolution.toMinutes()); + + logger.debug("\"timeSeries\" node: {}/{} with start time: {} and resolution {}", (i + 1), + listOfTimeSeries.getLength(), startTimeInstant.atZone(ZoneId.of("UTC")), resolution); + + NodeList listOfPoints = periodElement.getElementsByTagName("Point"); + + /* + * EntsoE changed their API on October 1 2024 so that they use the A03 curve type instead of A01. The + * difference between these curve types is that in A03 they don’t repeat an hour if it has the same + * price + * as the previous hour. + */ + int pointNr = 0; + for (int p = 0; p < numberOfDurations && resolution.equalsIgnoreCase(configResolution); p++) { + resolutionFound = true; + Node pointNode = listOfPoints.item(pointNr); + + int multiplier = p; + if (pointNode != null) { + Element pointElement = (Element) pointNode; + String price = pointElement.getElementsByTagName("price.amount").item(0).getTextContent(); + Double priceAsDouble = Double.parseDouble(price); + SpotPrice t = new SpotPrice(currency, measureUnit, priceAsDouble, startTimeInstant, multiplier, + resolution); + responseMap.put(t.getInstant(), t); + logger.trace("\"Point\" node: {}/{} with values: {} - {} {}/{}", (p + 1), numberOfDurations, + t.getInstant(), priceAsDouble, currency, measureUnit); + } + + Node nextPointNode = listOfPoints.item(pointNr + 1); + if (nextPointNode != null) { + Element nextPointElement = (Element) nextPointNode; + Node nextPositionNode = nextPointElement.getElementsByTagName("position").item(0); + if (nextPositionNode != null) { + int nextPosition = Integer.parseInt(nextPositionNode.getTextContent()); + if (nextPosition == p + 2) { + pointNr++; + } + } + } + + } + } + } + if (!resolutionFound) { + throw new EntsoeConfigurationException("Resolution " + configResolution + " not found in ENTSOE response"); + } + return responseMap; + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java new file mode 100644 index 00000000000..fe9c73f038c --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java @@ -0,0 +1,62 @@ +/** + * 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.entsoe.internal.client; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Miika Jukka - Initial contribution + * + */ +@NonNullByDefault +public class Request { + + private static DateTimeFormatter requestFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + + private static final String BASE_URL = "https://web-api.tp.entsoe.eu/api?"; + private static final String DOCUMENT_TYPE_PRICE = "A44"; + + private final String securityToken; + private final String area; + private final Instant periodStart; + private final Instant periodEnd; + + public Request(String securityToken, String area, Instant periodStart, Instant periodEnd) { + this.securityToken = securityToken; + this.area = area; + this.periodStart = periodStart; + this.periodEnd = periodEnd; + } + + public String toUrl() { + return urlBuilder(this.securityToken); + } + + @Override + public String toString() { + return urlBuilder("xxxxx-xxxxx-xxxxx"); + } + + private String urlBuilder(String securityToken) { + StringBuilder urlBuilder = new StringBuilder(BASE_URL).append("securityToken=").append(securityToken) + .append("&documentType=").append(DOCUMENT_TYPE_PRICE).append("&in_domain=").append(area) + .append("&out_domain=").append(area).append("&periodStart=") + .append(periodStart.atZone(ZoneOffset.UTC).format(requestFormat)).append("&periodEnd=") + .append(periodEnd.atZone(ZoneOffset.UTC).format(requestFormat)); + return urlBuilder.toString(); + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/SpotPrice.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/SpotPrice.java new file mode 100644 index 00000000000..24159da7ede --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/SpotPrice.java @@ -0,0 +1,93 @@ +/** + * 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.entsoe.internal.client; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.time.Period; +import java.time.format.DateTimeParseException; + +import javax.measure.Unit; +import javax.measure.quantity.Energy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; + +/** + * @author Jørgen Melhus - Initial contribution + */ +@NonNullByDefault +public class SpotPrice { + private String currency; + private Unit unit; + private Double price; + private Instant time; + + public SpotPrice(String currency, String unit, Double price, Instant start, int iteration, String resolution) + throws EntsoeResponseException { + this.currency = currency; + this.unit = convertEntsoeUnit(unit); + this.price = price; + this.time = calculateDateTime(start, iteration, resolution); + } + + private Instant calculateDateTime(Instant start, int iteration, String resolution) throws EntsoeResponseException { + try { + if (resolution.toUpperCase().startsWith("PT")) { + Duration d = Duration.parse(resolution).multipliedBy(iteration); + return start.plus(d); + } else if (resolution.toUpperCase().startsWith("P1")) { + return start.plus(Period.parse(resolution).multipliedBy(iteration)); + } + throw new EntsoeResponseException("Unknown resolution: " + resolution); + } catch (DateTimeParseException e) { + throw new EntsoeResponseException( + "DateTimeParseException (ENTSOE resolution: " + resolution + "): " + e.getMessage(), e); + } + } + + private Unit convertEntsoeUnit(String unit) throws EntsoeResponseException { + if ("MWh".equalsIgnoreCase(unit)) { + return Units.MEGAWATT_HOUR; + } + if ("kWh".equalsIgnoreCase(unit)) { + return Units.KILOWATT_HOUR; + } + if ("Wh".equalsIgnoreCase(unit)) { + return Units.WATT_HOUR; + } + + throw new EntsoeResponseException("Unit from ENTSO-E is unknown: " + unit); + } + + private BigDecimal getPrice(Unit toUnit) { + return new DecimalType(toUnit.getConverterTo(this.unit).convert(this.price)).toBigDecimal(); + } + + public State getState(Unit toUnit) { + try { + return new QuantityType<>(getPrice(toUnit) + " " + this.currency + "/" + toUnit.toString()); + } catch (IllegalArgumentException e) { + return new DecimalType(getPrice(toUnit)); + } + } + + public Instant getInstant() { + return this.time; + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeConfigurationException.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeConfigurationException.java new file mode 100644 index 00000000000..54cdf5b9195 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeConfigurationException.java @@ -0,0 +1,33 @@ +/** + * 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.entsoe.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Miika Jukka - Initial contribution + * + */ +@NonNullByDefault +public class EntsoeConfigurationException extends Exception { + + private static final long serialVersionUID = 1L; + + public EntsoeConfigurationException(String message) { + super(message); + } + + public EntsoeConfigurationException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseException.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseException.java new file mode 100644 index 00000000000..39f79372453 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseException.java @@ -0,0 +1,37 @@ +/** + * 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.entsoe.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Miika Jukka - Initial contribution + * + */ +@NonNullByDefault +public class EntsoeResponseException extends Exception { + + private static final long serialVersionUID = 1L; + + public EntsoeResponseException(String message) { + super(message); + } + + public EntsoeResponseException(String message, Throwable cause) { + super(message, cause); + } + + public EntsoeResponseException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseMapException.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseMapException.java new file mode 100644 index 00000000000..a81fdf3b9e9 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/exception/EntsoeResponseMapException.java @@ -0,0 +1,33 @@ +/** + * 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.entsoe.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Jørgen Melhus - Initial contribution + * + */ +@NonNullByDefault +public class EntsoeResponseMapException extends EntsoeConfigurationException { + + private static final long serialVersionUID = 1L; + + public EntsoeResponseMapException(String message) { + super(message); + } + + public EntsoeResponseMapException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..00003455d6c --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,13 @@ + + + + binding + ENTSO-E Binding + This is the binding for European Network of Transmission System Operators (ENTSO-E) to receive electricity + market spot prices. + + cloud + + diff --git a/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/i18n/entsoe.properties b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/i18n/entsoe.properties new file mode 100644 index 00000000000..ee4e53389a8 --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/i18n/entsoe.properties @@ -0,0 +1,142 @@ +# add-on + +addon.entsoe.name = ENTSO-E Binding +addon.entsoe.description = This is the binding for European Network of Transmission System Operators (ENTSO-E) to receive electricity market spot prices. + +# thing types + +thing-type.entsoe.day-ahead.label = Day-ahead prices +thing-type.entsoe.day-ahead.description = Hourly spot prices of the electricity market from European Network of Transmission System Operators (ENTSO-E) + +# thing types config + +thing-type.config.entsoe.day-ahead.area.label = Area +thing-type.config.entsoe.day-ahead.area.description = Choose from list or enter custom EIC area. See https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas and https://www.entsoe.eu/data/energy-identification-codes-eic/eic-approved-codes/ (Area Y, Bidding Zone) +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A39I = EE Estonia +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A44P = SE1 Swedish Elspot Area 1 +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A45N = SE2 Swedish Elspot Area 2 +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A46L = SE3 Swedish Elspot Area 3 +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A47J = SE4 Swedish Elspot Area 4 +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A48H = NO5 Norwegian Area Elspot Area 5 +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A50U = KALININGRAD_AREA Kaliningrad area +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A51S = BELARUS_AREA Belarus area +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A55K = LT_BEL_IMP_AREA Lithuania-Belarus Import Area +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A56I = LT_BEL_EXP_AREA Lithuania-Belarus Export Area +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A57G = GB_N2EX_PRICZONE Bidding Zone - Great Britain (N2EX) +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A58E = GB_APX_PRICEZONE Bidding Zone - Great Britain (APX) +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A59C = SEM Ireland and Northern Ireland +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A63L = DE_AT_LU Germany_Austria_Luxemburg +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A66F = IT-GR Italy Greece +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A67D = IT-NORTH_SI Italy North_Slovenia +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A68B = IT-NORTH_CH Italy North_Switzerland +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A70O = IT-CENTRE_NORTH Italy Centre-North +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A71M = IT-CENTRE_SOUTH Italy Centre-South +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A73I = IT-NORTH Italy North +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A74G = IT-SARDINIA Italy Sardinia +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A75E = IT-SICILY Italy Sicily +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A788 = IT-SOUTH Italy South +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A80L = IT-NORTH_AT Italy North_Austria +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A81J = IT-NORTH_FR Italy North_France +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A82H = DE_LU Germany_Luxemburg +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A877 = IT-MT Italy_Malta +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A885 = IT-SACO_AC Italy Saco_AC +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A893 = IT-SACODC Italy Saco_DC +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A90I = IT-MONFALCONE Italy Monfalcone +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A958 = ICELAND Member State Iceland +thing-type.config.entsoe.day-ahead.area.option.10Y1001A1001A990 = MOLDOVA Moldova +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--000182 = UA-IPS Ukraine IPS +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--000611 = IT-MONTENEGRO BIDDING ZONE ITALY-MONTENEGRO +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--00096J = IT-CALABRIA Italy Calabria +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--00098F = FR_UK_IFA_BZN IFA Bidding Zone +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--00100H = CAKOSTT Control Area Kosovo* +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--001219 = NO_FICT_AREA_2A Norwegian Fictitious Area 2A +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--00146U = NORDIC_SYNC_AREA Nordic Synchronous Area +thing-type.config.entsoe.day-ahead.area.option.10Y1001C--00148Q = SE3A Swedish Fictitious Area SE3A +thing-type.config.entsoe.day-ahead.area.option.10YAL-KESH-----5 = AL Albania +thing-type.config.entsoe.day-ahead.area.option.10YAT-APG------L = AT Austria +thing-type.config.entsoe.day-ahead.area.option.10YBA-JPCC-----D = BA Bosnia and Herzegovina +thing-type.config.entsoe.day-ahead.area.option.10YBE----------2 = BE Belgium +thing-type.config.entsoe.day-ahead.area.option.10YCA-BULGARIA-R = BG Bulgaria +thing-type.config.entsoe.day-ahead.area.option.10YCH-SWISSGRIDZ = CH Switzerland +thing-type.config.entsoe.day-ahead.area.option.10YCS-CG-TSO---S = ME Montenegro +thing-type.config.entsoe.day-ahead.area.option.10YCS-SERBIATSOV = RS Serbia +thing-type.config.entsoe.day-ahead.area.option.10YCY-1001A0003J = CY Cyprus +thing-type.config.entsoe.day-ahead.area.option.10YCZ-CEPS-----N = CZ Czech Republic +thing-type.config.entsoe.day-ahead.area.option.10YDK-1--------W = DK1 Denmark DK1 +thing-type.config.entsoe.day-ahead.area.option.10YDK-2--------M = DK2 Denmark DK2 +thing-type.config.entsoe.day-ahead.area.option.10YES-REE------0 = ES Spain +thing-type.config.entsoe.day-ahead.area.option.10YFI-1--------U = FI Finland +thing-type.config.entsoe.day-ahead.area.option.10YFR-RTE------C = FR France +thing-type.config.entsoe.day-ahead.area.option.10YGB----------A = GB Great Britain +thing-type.config.entsoe.day-ahead.area.option.10YGR-HTSO-----Y = GR Greece +thing-type.config.entsoe.day-ahead.area.option.10YHR-HEP------M = HR Croatia +thing-type.config.entsoe.day-ahead.area.option.10YHU-MAVIR----U = HU Hungary +thing-type.config.entsoe.day-ahead.area.option.10YLT-1001A0008Q = LT Lithuania +thing-type.config.entsoe.day-ahead.area.option.10YLV-1001A00074 = LV Latvia +thing-type.config.entsoe.day-ahead.area.option.10YMK-MEPSO----8 = MK FYROM +thing-type.config.entsoe.day-ahead.area.option.10YNL----------L = NL Netherlands +thing-type.config.entsoe.day-ahead.area.option.10YNO-1--------2 = NO1 Norwegian Area Elspot Area 1 +thing-type.config.entsoe.day-ahead.area.option.10YNO-2--------T = NO2 Norwegian Area Elspot Area 2 +thing-type.config.entsoe.day-ahead.area.option.10YNO-3--------J = NO3 Norwegian Area Elspot Area 3 +thing-type.config.entsoe.day-ahead.area.option.10YNO-4--------9 = NO4 Norwegian Area Elspot Area 4 +thing-type.config.entsoe.day-ahead.area.option.10YPL-AREA-----S = PL Poland +thing-type.config.entsoe.day-ahead.area.option.10YPT-REN------W = PT Portugal +thing-type.config.entsoe.day-ahead.area.option.10YRO-TEL------P = RO Romania +thing-type.config.entsoe.day-ahead.area.option.10YSI-ELES-----O = SI Slovenia +thing-type.config.entsoe.day-ahead.area.option.10YSK-SEPS-----K = SK Slovak Republic +thing-type.config.entsoe.day-ahead.area.option.10YTR-TEIAS----W = TEIAS_AREA TEIAS Area +thing-type.config.entsoe.day-ahead.area.option.10YUA-WEPS-----0 = UA-BEI Ukrainian Area of Burshtyn island +thing-type.config.entsoe.day-ahead.area.option.14Y----0000041-W = AT-EXAAMC Virtual Bidding Zone EXAA +thing-type.config.entsoe.day-ahead.area.option.14YCCPADATENMLDW = CCPACCP1 CCP Austria GmbH +thing-type.config.entsoe.day-ahead.area.option.14YEXAADATENMLDU = EXAACCP EXAA Abwicklungsstelle für Energieprodukte AG +thing-type.config.entsoe.day-ahead.area.option.17Y000000930808E = FR_UK_IFA2000_1 vhub_UK_IFA2000_link1 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930809C = FR_FR_IFA2000_1 vhub_FR_IFA2000_link1 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930810R = FR_UK_IFA2000_2 vhub_UK_IFA2000_link2 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930811P = FR_FR_IFA2000_2 vhub_FR_IFA2000_link2 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930814J = FR_UK_IFA2 vhub_UK_IFA2 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930815H = FR_FR_IFA2 vhub_FR_IFA2 +thing-type.config.entsoe.day-ahead.area.option.17Y000000930816F = FR_IT_SAV_PIE vhub_IT_Savoie_Piemont +thing-type.config.entsoe.day-ahead.area.option.17Y000000930817D = FR_FR_SAV_PIE vhub_FR_Savoie_Piemont +thing-type.config.entsoe.day-ahead.area.option.17Y0000009369493 = FR_UK_IFA2_V_BZN IFA2_virtual_BZN +thing-type.config.entsoe.day-ahead.area.option.44Y-00000000160K = FI_FS Fingrid Oyj +thing-type.config.entsoe.day-ahead.area.option.44Y-00000000161I = FI_EL Fingrid Oyj +thing-type.config.entsoe.day-ahead.area.option.45Y000000000001C = DKW-NO2 DKW-NO2 virtual Bidding Zone Border +thing-type.config.entsoe.day-ahead.area.option.45Y000000000002A = DKW-SE3 DKW-SE3 virtual Bidding Zone Border +thing-type.config.entsoe.day-ahead.area.option.45Y0000000000038 = DKW-DKE DKW-DKE virtual Bidding Zone Border +thing-type.config.entsoe.day-ahead.area.option.46Y000000000001Y = SE3_FS SE3_FS (SE3, Fennoskan) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000002W = SE3_KS SE3_KS (SE3, Kontiskan) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000003U = SE4_SP SE4_SP (SE4, Swepol link) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000004S = SE4_NB SE4_NB (SE4, Nordbalt) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000005Q = SE4_BC SE4_BC (SE4, Baltic Cable) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000007M = CUT_AREA_SE3LS Cut area SE3LS +thing-type.config.entsoe.day-ahead.area.option.46Y000000000008K = CUT_AREA_SE3 Cut area SE3 +thing-type.config.entsoe.day-ahead.area.option.46Y000000000009I = CUTCOR_SE3LS-SE3 Cut corridor SE3LS-SE3 +thing-type.config.entsoe.day-ahead.area.option.46Y000000000015N = VBZ_SE3-SE4_ACSW Virtual bidding zone border SE3-SE4 AC+SWL +thing-type.config.entsoe.day-ahead.area.option.46Y000000000016L = VBZ_SE4-SE3_ACSW Virtual bidding zone border SE4-SE3 AC+SWL +thing-type.config.entsoe.day-ahead.area.option.46Y000000000017J = SE3_SWL SE3_SWL (SE3, Sydvastlanken) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000018H = SE4_SWL SE4_SWL (SE4, Sydvastlanken) +thing-type.config.entsoe.day-ahead.area.option.46Y000000000019F = CUTCOR_SE3A-SE3 Cut corridor SE3A-SE3 +thing-type.config.entsoe.day-ahead.area.option.50Y0JVU59B4JWQCU = NO_NO2NSL Virtual Bidding Zone NO2NSL +thing-type.config.entsoe.day-ahead.area.option.50Y73EMZ34CQL9AJ = NO_NO2_NL Virtual Bidding Zone NO2-NL +thing-type.config.entsoe.day-ahead.area.option.50YCUY85S1HH29EK = NO_NO2_DK1 Virtual Bidding Zone NO2-DK1 +thing-type.config.entsoe.day-ahead.area.option.50Y-HTS3792HUOAC = NO_NO2-GB Virtual bidding Zone NO2-GB +thing-type.config.entsoe.day-ahead.area.option.50YNBFFTWZRAHA3P = NO_NO2-DE Virtual bidding Zone NO2-DE +thing-type.config.entsoe.day-ahead.area.option.67Y-VISKOL-2003T = VISKOL2003 VISKOL DOO NOVI SAD SRBIJA +thing-type.config.entsoe.day-ahead.historicDays.label = Historic Data +thing-type.config.entsoe.day-ahead.historicDays.description = Specify number of days to download historic data of energy spot prices (historic data will get exchange rate of today) +thing-type.config.entsoe.day-ahead.requestTimeout.label = Request Timeout +thing-type.config.entsoe.day-ahead.requestTimeout.description = Request timeout value in seconds +thing-type.config.entsoe.day-ahead.resolution.label = Resolution +thing-type.config.entsoe.day-ahead.resolution.description = Data resolution +thing-type.config.entsoe.day-ahead.resolution.option.PT15M = 15 minutes +thing-type.config.entsoe.day-ahead.resolution.option.PT30M = 30 minutes +thing-type.config.entsoe.day-ahead.resolution.option.PT60M = 60 minutes +thing-type.config.entsoe.day-ahead.securityToken.label = Security Token +thing-type.config.entsoe.day-ahead.securityToken.description = Security token for ENTSO-E API +thing-type.config.entsoe.day-ahead.spotPricesAvailableCetHour.label = Publish Hour +thing-type.config.entsoe.day-ahead.spotPricesAvailableCetHour.description = CET hour spot prices will get published on ENTSO-E. Usually at 13 CET. + +# channel types + +channel-type.entsoe.spot-price.label = Spot Price +channel-type.entsoe.trigger-prices-received.label = Trigger Prices Received diff --git a/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..6845becf62c --- /dev/null +++ b/bundles/org.openhab.binding.entsoe/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,194 @@ + + + + + + + Hourly spot prices of the electricity market from European Network of Transmission System Operators + (ENTSO-E) + WebService + + + + + + + + password + + Security token for ENTSO-E API + + + + Choose from list or enter custom EIC area. See + https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas + and + https://www.entsoe.eu/data/energy-identification-codes-eic/eic-approved-codes/ (Area Y, Bidding Zone) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + Specify number of days to download historic data of energy spot prices (historic data will get exchange + rate of today) + + + + PT60M + Data resolution + true + + + + + + + + + 13 + CET hour spot prices will get published on ENTSO-E. Usually at 13 CET. + true + + + + 30 + Request timeout value in seconds + true + + + + + + Number:EnergyPrice + + Price + + + + + trigger + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index b89c5cc8ea1..b3a95a4c86b 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -138,6 +138,7 @@ org.openhab.binding.enigma2 org.openhab.binding.enocean org.openhab.binding.enphase + org.openhab.binding.entsoe org.openhab.binding.enturno org.openhab.binding.ephemeris org.openhab.binding.epsonprojector