diff --git a/custom_components/energidataservice/__init__.py b/custom_components/energidataservice/__init__.py index 9bfbaab..8292138 100644 --- a/custom_components/energidataservice/__init__.py +++ b/custom_components/energidataservice/__init__.py @@ -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 diff --git a/custom_components/energidataservice/api.py b/custom_components/energidataservice/api.py index 392f3cd..4ddd6f6 100644 --- a/custom_components/energidataservice/api.py +++ b/custom_components/energidataservice/api.py @@ -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 diff --git a/custom_components/energidataservice/connectors/__init__.py b/custom_components/energidataservice/connectors/__init__.py index a3d02be..cf1e508 100644 --- a/custom_components/energidataservice/connectors/__init__.py +++ b/custom_components/energidataservice/connectors/__init__.py @@ -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) @@ -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 diff --git a/custom_components/energidataservice/connectors/energidataservice/__init__.py b/custom_components/energidataservice/connectors/energidataservice/__init__.py index 57f55e7..898519a 100644 --- a/custom_components/energidataservice/connectors/energidataservice/__init__.py +++ b/custom_components/energidataservice/connectors/energidataservice/__init__.py @@ -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 diff --git a/custom_components/energidataservice/connectors/energidataservice/regions.py b/custom_components/energidataservice/connectors/energidataservice/regions.py index 9b9de1e..6ebc4b5 100644 --- a/custom_components/energidataservice/connectors/energidataservice/regions.py +++ b/custom_components/energidataservice/connectors/energidataservice/regions.py @@ -3,9 +3,6 @@ REGIONS = { "DK1", "DK2", - "SE3", - "SE4", - "NO2", } CO2REGIONS = { diff --git a/custom_components/energidataservice/connectors/fixedprice/__init__.py b/custom_components/energidataservice/connectors/fixedprice/__init__.py index 7384ca8..a40500f 100644 --- a/custom_components/energidataservice/connectors/fixedprice/__init__.py +++ b/custom_components/energidataservice/connectors/fixedprice/__init__.py @@ -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 diff --git a/custom_components/energidataservice/connectors/nordpool/__init__.py b/custom_components/energidataservice/connectors/nordpool/__init__.py new file mode 100644 index 0000000..a3708a2 --- /dev/null +++ b/custom_components/energidataservice/connectors/nordpool/__init__.py @@ -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 diff --git a/custom_components/energidataservice/connectors/nordpool/mapping.py b/custom_components/energidataservice/connectors/nordpool/mapping.py new file mode 100644 index 0000000..34a598b --- /dev/null +++ b/custom_components/energidataservice/connectors/nordpool/mapping.py @@ -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 diff --git a/custom_components/energidataservice/connectors/nordpool/regions.py b/custom_components/energidataservice/connectors/nordpool/regions.py new file mode 100644 index 0000000..13891bf --- /dev/null +++ b/custom_components/energidataservice/connectors/nordpool/regions.py @@ -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", +} diff --git a/custom_components/energidataservice/const.py b/custom_components/energidataservice/const.py index 67d48a6..956db61 100644 --- a/custom_components/energidataservice/const.py +++ b/custom_components/energidataservice/const.py @@ -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], } diff --git a/custom_components/energidataservice/sensor.py b/custom_components/energidataservice/sensor.py index 77c372e..55dd012 100644 --- a/custom_components/energidataservice/sensor.py +++ b/custom_components/energidataservice/sensor.py @@ -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