Skip to content

Commit

Permalink
Merge pull request #618 from MTrab/Ressurect-Nordpool
Browse files Browse the repository at this point in the history
Reintroducing Nordpool connector
  • Loading branch information
MTrab authored Oct 16, 2024
2 parents d344b15 + ff91daf commit 556b401
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 21 deletions.
10 changes: 9 additions & 1 deletion custom_components/energidataservice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ async def _setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
rand_sec = randint(0, 59)
api = APIConnector(hass, entry, rand_min, rand_sec)
await api.initialize()
# await api.updateco2()

connectors = api._connectors.get_connectors(api._region.region)
for connector in connectors:
if connector.co2regions == []:
api.has_co2 = False
else:
api.has_co2 = True
break

hass.data[DOMAIN][entry.entry_id] = api
use_forecast = entry.options.get(CONF_ENABLE_FORECAST) or False

Expand Down
1 change: 1 addition & 0 deletions custom_components/energidataservice/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def __init__(

self.co2 = None
self.co2_refresh = None
self.has_co2 = False
self.today = None
self.api_today = None
self.tomorrow = None
Expand Down
16 changes: 12 additions & 4 deletions custom_components/energidataservice/connectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ async def load_connectors(self) -> None:
for module in sorted(modules):
mod_path = f"{dirname(__file__)}/{module}"
if isdir(mod_path) and not module.endswith("__pycache__"):
Connector = namedtuple("Connector", "module namespace regions")
Connector = namedtuple(
"Connector", "module namespace regions co2regions"
)
_LOGGER.debug("Adding module %s in path %s", module, mod_path)
api_ns = f".{module}"

mod = await self.hass.async_add_executor_job(
importlib.import_module, api_ns, __name__
)
con = Connector(module, f".connectors{api_ns}", mod.REGIONS)
con = Connector(
module, f".connectors{api_ns}", mod.REGIONS, mod.CO2REGIONS
)

if hasattr(mod, "EXTRA_REGIONS"):
REGIONS.update(mod.EXTRA_REGIONS)
Expand All @@ -61,7 +65,11 @@ def get_connectors(self, region: str) -> list:
for connector in self._connectors:
_LOGGER.debug("%s = %s", connector, connector.regions)
if region in connector.regions:
Connector = namedtuple("Connector", "module namespace")
connectors.append(Connector(connector.module, connector.namespace))
Connector = namedtuple("Connector", "module namespace co2regions")
connectors.append(
Connector(
connector.module, connector.namespace, connector.co2regions
)
)

return connectors
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

DEFAULT_CURRENCY = "EUR"

__all__ = ["REGIONS", "Connector", "DEFAULT_CURRENCY"]
__all__ = ["REGIONS", "Connector", "DEFAULT_CURRENCY", "CO2REGIONS"]


def prepare_data(indata, date, tz) -> list: # pylint: disable=invalid-name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
REGIONS = {
"DK1",
"DK2",
"SE3",
"SE4",
"NO2",
}

CO2REGIONS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
SOURCE_NAME = "Fixed Price"

DEFAULT_CURRENCY = None

__all__ = ["REGIONS", "Connector", "DEFAULT_CURRENCY"]
CO2REGIONS = []
__all__ = ["REGIONS", "Connector", "DEFAULT_CURRENCY", "CO2REGIONS"]


def prepare_data(value, date, tz) -> list: # pylint: disable=invalid-name
Expand Down
167 changes: 167 additions & 0 deletions custom_components/energidataservice/connectors/nordpool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Nordpool connector."""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timedelta

import homeassistant.util.dt as dt_util
import pytz

from ...const import INTERVAL
from .mapping import map_region
from .regions import REGIONS

_LOGGER = logging.getLogger(__name__)

# BASE_URL = (
# "https://www.nordpoolgroup.com/api/marketdata/page/10?currency=EUR&endDate=%s"
# )

BASE_URL = "https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices?currency=EUR&date={}&market=DayAhead&deliveryArea={}"

SOURCE_NAME = "Nord Pool"

DEFAULT_CURRENCY = "EUR"

TIMEZONE = pytz.timezone("Europe/Stockholm")
CO2REGIONS = []

__all__ = ["REGIONS", "Connector", "DEFAULT_CURRENCY", "CO2REGIONS"]


def prepare_data(indata, date, tz) -> list: # pylint: disable=invalid-name
"""Get today prices."""
local_tz = dt_util.get_default_time_zone()
reslist = []
for dataset in indata:
tmpdate = datetime.fromisoformat(dataset["HourUTC"]).astimezone(local_tz)
tmp = INTERVAL(dataset["SpotPriceEUR"], tmpdate)
if date in tmp.hour.strftime("%Y-%m-%d"):
reslist.append(tmp)

return reslist


class Connector:
"""Define Nordpool Connector Class."""

def __init__(
self, regionhandler, client, tz, config # pylint: disable=invalid-name
) -> None:
"""Init API connection to Nordpool Group."""
self.config = config
self.regionhandler = regionhandler
self.client = client
self._result = {}
self._tz = tz
self.status = 200

async def async_get_spotprices(self) -> None:
"""Fetch latest spotprices, excl. VAT and tariff."""
# yesterday = datetime.now() - timedelta(days=1)
yesterday = datetime.now() - timedelta(days=1)
today = datetime.now()
tomorrow = datetime.now() + timedelta(days=1)
jobs = [
self._fetch(yesterday),
self._fetch(today),
self._fetch(tomorrow),
]

res = await asyncio.gather(*jobs)
raw = []

for i in res:
raw = raw + self._parse_json(i)

self._result = raw

_LOGGER.debug("Dataset for %s:", self.regionhandler.region)
_LOGGER.debug(self._result)

async def _fetch(self, enddate: datetime) -> str:
"""Fetch data from API."""
if self.regionhandler.api_region:
region = self.regionhandler.api_region
else:
region = self.regionhandler.region

url = BASE_URL.format(enddate.strftime("%Y-%m-%d"), region)

_LOGGER.debug(
"Request URL for %s via Nordpool: %s",
(self.regionhandler.api_region or self.regionhandler.region),
url,
)
resp = await self.client.get(url)

if resp.status == 400:
_LOGGER.error("API returned error 400, Bad Request!")
return {}
elif resp.status == 411:
_LOGGER.error("API returned error 411, Invalid Request!")
return {}
elif resp.status == 200:
res = await resp.json()
_LOGGER.debug("Response for %s:", self.regionhandler.region)
_LOGGER.debug(res)
return res
elif resp.status == 204:
return {}
elif resp.status == 500:
_LOGGER.warning("Server blocked request")
else:
_LOGGER.error("API returned error %s", str(resp.status))
return {}

def _parse_json(self, data) -> list:
"""Parse json response."""
# Timezone for data from Nord Pool Group are "Europe/Stockholm"

if not "multiAreaEntries" in data:
return []

if self.regionhandler.api_region:
region = self.regionhandler.api_region
else:
region = self.regionhandler.region

region_data = []

for entry in data["multiAreaEntries"]:
start_hour = entry["deliveryStart"]
value = entry["entryPerArea"][region]
region_data.append(
{
"HourUTC": start_hour,
"SpotPriceEUR": value,
}
)

return region_data

@staticmethod
def _conv_to_float(value) -> float | None:
"""Convert numbers to float. Return infinity, if conversion fails."""
try:
return float(value.replace(",", ".").replace(" ", ""))
except ValueError:
return None

@property
def today(self) -> list:
"""Return raw dataset for today."""
date = datetime.now().strftime("%Y-%m-%d")
return prepare_data(self._result, date, self._tz)

@property
def tomorrow(self) -> list:
"""Return raw dataset for today."""
date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
data = prepare_data(self._result, date, self._tz)
if len(data) > 20:
return data
else:
return None
23 changes: 23 additions & 0 deletions custom_components/energidataservice/connectors/nordpool/mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Do some mappings for region naming."""

from __future__ import annotations

from ...utils.regionhandler import RegionHandler

_REGION_MAP = {
"DE": "DE-LU",
"NO1": "Oslo",
"NO2": "Kr.Sand",
"NO3": "Molde",
"NO4": "Tromsø",
"NO5": "Bergen",
"LU": "DE-LU",
}


def map_region(region: RegionHandler) -> RegionHandler():
"""Map integration region to API region."""
if region.region in _REGION_MAP:
region.set_api_region(_REGION_MAP[region.region])

return region
23 changes: 23 additions & 0 deletions custom_components/energidataservice/connectors/nordpool/regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Define valid zones for Nord Pool."""

REGIONS = {
"SE1",
"SE2",
"SE3",
"SE4",
"NO1",
"NO2",
"NO3",
"NO4",
"NO5",
"FI",
"EE",
"LV",
"LT",
"FR",
"NL",
"BE",
"AT",
"DE",
"LU",
}
16 changes: 16 additions & 0 deletions custom_components/energidataservice/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,25 @@
REGIONS = {
"DK1": [CURRENCY_LIST["DKK"], "Denmark", "West of the great belt", 0.25],
"DK2": [CURRENCY_LIST["DKK"], "Denmark", "East of the great belt", 0.25],
"FI": [CURRENCY_LIST["EUR"], "Finland", "Finland", 0.24],
"EE": [CURRENCY_LIST["EUR"], "Estonia", "Estonia", 0.20],
"LT": [CURRENCY_LIST["EUR"], "Lithuania", "Lithuania", 0.21],
"LV": [CURRENCY_LIST["EUR"], "Latvia", "Latvia", 0.21],
"NO1": [CURRENCY_LIST["NOK"], "Norway", "Oslo", 0.25],
"NO2": [CURRENCY_LIST["NOK"], "Norway", "Kristiansand", 0.25],
"NO3": [CURRENCY_LIST["NOK"], "Norway", "Molde, Trondheim", 0.25],
"NO4": [CURRENCY_LIST["NOK"], "Norway", "Tromsø", 0.25],
"NO5": [CURRENCY_LIST["NOK"], "Norway", "Bergen", 0.25],
"SE1": [CURRENCY_LIST["SEK"], "Sweden", "Luleå", 0.25],
"SE2": [CURRENCY_LIST["SEK"], "Sweden", "Sundsvall", 0.25],
"SE3": [CURRENCY_LIST["SEK"], "Sweden", "Stockholm", 0.25],
"SE4": [CURRENCY_LIST["SEK"], "Sweden", "Malmö", 0.25],
"FR": [CURRENCY_LIST["EUR"], "France", "France", 0.055],
"NL": [CURRENCY_LIST["EUR"], "Netherlands", "Netherlands", 0.21],
"BE": [CURRENCY_LIST["EUR"], "Belgium", "Belgium", 0.21],
"AT": [CURRENCY_LIST["EUR"], "Austria", "Austria", 0.20],
"DE": [CURRENCY_LIST["EUR"], "Germany", "Germany", 0.19],
"LU": [CURRENCY_LIST["EUR"], "Luxemburg", "Luxemburg", 0.08],
"FIXED": [CURRENCY_LIST["NONE"], "Fixed Price", "Fixed Price", 0.0],
}

Expand Down
23 changes: 13 additions & 10 deletions custom_components/energidataservice/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,19 @@ def _setup(hass, config: ConfigEntry, add_devices):
sens = EnergidataserviceSensor(config, hass, region, this_sensor)
add_devices([sens])

co2_sensor = SensorEntityDescription(
key="EnergiDataService_co2",
device_class=None,
icon="mdi:molecule-co2",
name=config.data.get(CONF_NAME) + " CO2",
state_class=SensorStateClass.MEASUREMENT,
last_reset=None,
)
sens = EnergidataserviceCO2Sensor(config, hass, region, co2_sensor)
add_devices([sens])
api = hass.data[DOMAIN][config.entry_id]

if api.has_co2:
co2_sensor = SensorEntityDescription(
key="EnergiDataService_co2",
device_class=None,
icon="mdi:molecule-co2",
name=config.data.get(CONF_NAME) + " CO2",
state_class=SensorStateClass.MEASUREMENT,
last_reset=None,
)
sens = EnergidataserviceCO2Sensor(config, hass, region, co2_sensor)
add_devices([sens])


@callback
Expand Down

0 comments on commit 556b401

Please sign in to comment.