Skip to content

Commit

Permalink
Merge pull request #413 from developerfromjokela/master
Browse files Browse the repository at this point in the history
New API integration
  • Loading branch information
Hellowlol authored Oct 13, 2024
2 parents 79944c9 + a1332b3 commit d81756f
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 82 deletions.
2 changes: 1 addition & 1 deletion custom_components/nordpool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


NAME = DOMAIN
VERSION = "0.0.14"
VERSION = "0.0.15"
ISSUEURL = "https://github.com/custom-components/nordpool/issues"

STARTUP = f"""
Expand Down
138 changes: 116 additions & 22 deletions custom_components/nordpool/aio_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@
from collections import defaultdict
from datetime import date, datetime, timedelta

from homeassistant.util import dt as dt_utils
from dateutil.parser import parse as parse_dt
import backoff
import aiohttp
import backoff
from dateutil.parser import parse as parse_dt
from homeassistant.util import dt as dt_utils
from nordpool.base import CurrencyMismatch
from nordpool.elspot import Prices
from pytz import timezone, utc

from .misc import add_junk, exceptions_raiser
from .misc import add_junk

_LOGGER = logging.getLogger(__name__)


tzs = {
"DK1": "Europe/Copenhagen",
"DK2": "Europe/Copenhagen",
"FI": "Europe/Helsinki",
"EE": "Europe/Tallinn",
"LT": "Europe/Vilnius",
"LV": "Europe/Riga",
"Oslo": "Europe/Oslo",
"Kr.sand": "Europe/Oslo",
"Bergen": "Europe/Oslo",
"Molde": "Europe/Oslo",
"Tr.heim": "Europe/Oslo",
"Tromsø": "Europe/Oslo",
"NO1": "Europe/Oslo",
"NO2": "Europe/Oslo",
"NO3": "Europe/Oslo",
"NO4": "Europe/Oslo",
"NO5": "Europe/Oslo",
"SE1": "Europe/Stockholm",
"SE2": "Europe/Stockholm",
"SE3": "Europe/Stockholm",
Expand All @@ -37,10 +37,9 @@
"NL": "Europe/Amsterdam",
"BE": "Europe/Brussels",
"AT": "Europe/Vienna",
"DE-LU": "Europe/Berlin",
"GER": "Europe/Berlin",
}


# List of page index for hourly data
# Some are disabled as they don't contain the other currencies, NOK etc,
# or there are some issues with data parsing for some ones' DataStartdate.
Expand Down Expand Up @@ -140,7 +139,7 @@ async def join_result_for_correct_time(results, dt):
for val in values:
local = val["start"].astimezone(zone)
local_end = val["end"].astimezone(zone)
if start_of_day <= local and local <= end_of_day:
if start_of_day <= local <= end_of_day:
if local == local_end:
_LOGGER.info(
"Hour has the same start and end, most likly due to dst change %s exluded this hour",
Expand All @@ -161,28 +160,120 @@ def __init__(self, currency, client, timeezone=None):
super().__init__(currency)
self.client = client
self.timeezone = timeezone
self.API_URL_CURRENCY = "https://www.nordpoolgroup.com/api/marketdata/page/%s"
(self.HOURLY, self.DAILY, self.WEEKLY, self.MONTHLY, self.YEARLY) = ("DayAheadPrices", "AggregatePrices",
"AggregatePrices", "AggregatePrices",
"AggregatePrices")
self.API_URL = "https://dataportal-api.nordpoolgroup.com/api/%s"

async def _io(self, url, **kwargs):

resp = await self.client.get(url, params=kwargs)
_LOGGER.debug("requested %s %s", resp.url, kwargs)

if resp.status == 204:
return None

return await resp.json()

async def _fetch_json(self, data_type, end_date=None):
def _parse_dt(self, time_str):
''' Parse datetimes to UTC from Stockholm time, which Nord Pool uses. '''
time = parse_dt(time_str, tzinfos={"Z": timezone("Europe/Stockholm")})
if time.tzinfo is None:
return timezone('Europe/Stockholm').localize(time).astimezone(utc)
return time.astimezone(utc)

def _parse_json(self, data, areas=None):
"""
Parse json response from fetcher.
Returns dictionary with
- start time
- end time
- update time
- currency
- dictionary of areas, based on selection
- list of values (dictionary with start and endtime and value)
- possible other values, such as min, max, average for hourly
"""

if areas is None:
areas = []
# If areas isn't a list, make it one

if not isinstance(areas, list):
areas = list(areas)

if data.get("status", 200) != 200 and "version" not in data:
raise Exception(f"Invalid response from Nordpool API: {data}")

# Update currency from data
currency = data['currency']

# Ensure that the provided currency match the requested one
if currency != self.currency:
raise CurrencyMismatch

start_time = None
end_time = None

if len(data['multiAreaEntries']) > 0:
start_time = self._parse_dt(data['multiAreaEntries'][0]['deliveryStart'])
end_time = self._parse_dt(data['multiAreaEntries'][-1]['deliveryEnd'])
updated = self._parse_dt(data['updatedAt'])

area_data = {}

# Loop through response rows
for r in data['multiAreaEntries']:
row_start_time = self._parse_dt(r['deliveryStart'])
row_end_time = self._parse_dt(r['deliveryEnd'])

# Loop through columns
for area_key in r['entryPerArea'].keys():
area_price = r['entryPerArea'][area_key]
# If areas is defined and name isn't in areas, skip column
if area_key not in areas:
continue

# If name isn't in area_data, initialize dictionary
if area_key not in area_data:
area_data[area_key] = {
'values': [],
}

# Append dictionary to value list
area_data[area_key]['values'].append({
'start': row_start_time,
'end': row_end_time,
'value': self._conv_to_float(area_price),
})

return {
'start': start_time,
'end': end_time,
'updated': updated,
'currency': currency,
'areas': area_data
}

async def _fetch_json(self, data_type, end_date=None, areas=None):
"""Fetch JSON from API"""
# If end_date isn't set, default to tomorrow
if areas is None or len(areas) == 0:
raise Exception("Cannot query with empty areas")
if end_date is None:
end_date = date.today() + timedelta(days=1)
# If end_date isn't a date or datetime object, try to parse a string
if not isinstance(end_date, date) and not isinstance(end_date, datetime):
end_date = parse_dt(end_date)



return await self._io(
self.API_URL % data_type,
currency=self.currency,
endDate=end_date.strftime("%d-%m-%Y"),
market="DayAhead",
deliveryArea=",".join(areas),
date=end_date.strftime("%Y-%m-%d"),
)

# Add more exceptions as we find them. KeyError is raised when the api return
Expand Down Expand Up @@ -220,14 +311,14 @@ async def fetch(self, data_type, end_date=None, areas=None):
tomorrow = datetime.now() + timedelta(days=1)

jobs = [
self._fetch_json(data_type, yesterday),
self._fetch_json(data_type, today),
self._fetch_json(data_type, tomorrow),
self._fetch_json(data_type, yesterday, areas),
self._fetch_json(data_type, today, areas),
self._fetch_json(data_type, tomorrow, areas),
]

res = await asyncio.gather(*jobs)
raw = [await self._async_parse_json(i, areas) for i in res]
raw = [await self._async_parse_json(i, areas) for i in res if i]

return await join_result_for_correct_time(raw, end_date)

async def _async_parse_json(self, data, areas):
Expand Down Expand Up @@ -269,6 +360,9 @@ async def yearly(self, end_date=None, areas=None):

def _conv_to_float(self, s):
"""Convert numbers to float. Return infinity, if conversion fails."""
# Skip if already float
if isinstance(s, float):
return s
try:
return float(s.replace(",", ".").replace(" ", ""))
except ValueError:
Expand Down
4 changes: 3 additions & 1 deletion custom_components/nordpool/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

_LOGGER = logging.getLogger(__name__)

stockholm_tz = timezone("Europe/Stockholm")


def exceptions_raiser():
"""Utility to check that all exceptions are raised."""
Expand Down Expand Up @@ -50,7 +52,7 @@ def add_junk(d):

def stock(d):
"""convert datetime to stocholm time."""
return d.astimezone(timezone("Europe/Stockholm"))
return d.astimezone(stockholm_tz)


def start_of(d, typ_="hour"):
Expand Down
18 changes: 8 additions & 10 deletions custom_components/nordpool/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@
"EE": ["EUR", "Estonia", 0.22],
"LT": ["EUR", "Lithuania", 0.21],
"LV": ["EUR", "Latvia", 0.21],
"Oslo": ["NOK", "Norway", 0.25],
"Kr.sand": ["NOK", "Norway", 0.25],
"Bergen": ["NOK", "Norway", 0.25],
"Molde": ["NOK", "Norway", 0.25],
"Tr.heim": ["NOK", "Norway", 0.25],
"Tromsø": ["NOK", "Norway", 0.25],
"NO1": ["NOK", "Norway", 0.25],
"NO2": ["NOK", "Norway", 0.25],
"NO3": ["NOK", "Norway", 0.25],
"NO4": ["NOK", "Norway", 0.25],
"NO5": ["NOK", "Norway", 0.25],
"SE1": ["SEK", "Sweden", 0.25],
"SE2": ["SEK", "Sweden", 0.25],
"SE3": ["SEK", "Sweden", 0.25],
Expand All @@ -54,17 +53,16 @@
"NL": ["EUR", "Netherlands", 0.21],
"BE": ["EUR", "Belgium", 0.06],
"AT": ["EUR", "Austria", 0.20],
# Tax is disabled for now, i need to split the areas
# to handle the tax.
"DE-LU": ["EUR", "Germany and Luxembourg", 0],
# Unsure about tax rate, correct if wrong
"GER": ["EUR", "Germany", 0.23],
}

# Needed incase a user wants the prices in non local currency
_CURRENCY_TO_LOCAL = {"DKK": "Kr", "NOK": "Kr", "SEK": "Kr", "EUR": "€"}
_CURRENTY_TO_CENTS = {"DKK": "Øre", "NOK": "Øre", "SEK": "Öre", "EUR": "c"}

DEFAULT_CURRENCY = "NOK"
DEFAULT_REGION = "Kr.sand"
DEFAULT_REGION = list(_REGIONS.keys())[0]
DEFAULT_NAME = "Elspot"


Expand Down
Loading

0 comments on commit d81756f

Please sign in to comment.