Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New API integration #413

Merged
merged 5 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading