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

first pass #127

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "ludeeus/container:integration-debian",
//"image": "ludeeus/container:integration-debian",
"image": "ghcr.io/ludeeus/devcontainer/integration:stable",
"name": "Nordpool integration development",
"context": "..",
"appPort": [
Expand All @@ -16,7 +17,7 @@
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.shell.linux": "/bin/bash",
//"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": false,
"python.linting.pylintEnabled": true,
Expand Down
138 changes: 105 additions & 33 deletions custom_components/nordpool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
from functools import partial
from random import randint

import aiohttp
import backoff
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import (async_call_later,
async_track_time_change)
from homeassistant.helpers.event import async_call_later, async_track_time_change
from homeassistant.util import dt as dt_utils
from pytz import timezone

from .aio_price import AioPrices
from .events import async_track_time_change_in_tz
from .misc import test_valid_nordpooldata, test_valid_nordpolldata2, stock
Hellowlol marked this conversation as resolved.
Show resolved Hide resolved

DOMAIN = "nordpool"
_LOGGER = logging.getLogger(__name__)
Expand All @@ -24,6 +26,34 @@
EVENT_NEW_DATA = "nordpool_update"
_CURRENCY_LIST = ["DKK", "EUR", "NOK", "SEK"]

_REGIONS = {
"DK1": ["DKK", "Denmark", 0.25],
"DK2": ["DKK", "Denmark", 0.25],
"FI": ["EUR", "Finland", 0.24],
"EE": ["EUR", "Estonia", 0.20],
"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],
"SE1": ["SEK", "Sweden", 0.25],
"SE2": ["SEK", "Sweden", 0.25],
"SE3": ["SEK", "Sweden", 0.25],
"SE4": ["SEK", "Sweden", 0.25],
# What zone is this?
"SYS": ["EUR", "System zone", 0.25],
"FR": ["EUR", "France", 0.055],
"NL": ["EUR", "Netherlands", 0.21],
"BE": ["EUR", "Belgium", 0.21],
"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],
}


CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)

Expand All @@ -48,39 +78,87 @@ def __init__(self, hass: HomeAssistant):
self._hass = hass
self._last_tick = None
self._data = defaultdict(dict)
self._tomorrow_valid = False
self.currency = []
self.listeners = []

async def _update(self, type_="today", dt=None):
async def _update(self, *args, type_="today", dt=None):
_LOGGER.debug("calling _update %s %s", type_, dt)
hass = self._hass
client = async_get_clientsession(hass)

ATTEMPTS = 0

if dt is None:
dt = dt_utils.now()

# We dont really need today and morrow
# when the region is in another timezone
# as we request data for 3 days anyway.
# Keeping this for now, but this should be changed.
for currency in self.currency:
@backoff.on_predicate(
backoff.constant, interval=60, logger=_LOGGER
) # Set better defaults, and add a give_up
@backoff.on_exception(backoff.expo, aiohttp.ClientError, logger=_LOGGER)
async def really_update(currency, end_date):
# Should be removed
""" "
nonlocal ATTEMPTS

if ATTEMPTS == 0:
ATTEMPTS += 1
raise aiohttp.ClientError
elif ATTEMPTS == 1:
ATTEMPTS += 1
return False
"""

func_now = dt_utils.now()
spot = AioPrices(currency, client)
data = await spot.hourly(end_date=dt)
if data:
self._data[currency][type_] = data["areas"]
data = await spot.hourly(end_date=end_date)
# We only verify the the areas that has the correct currency, example AT is always inf for all other currency then EUR
# Now this will fail for any users that has a non local currency for the region they selected.
# Thats a problem for another day..
regions_to_verify = [k for k, v in _REGIONS.items() if v[0] == currency]
data_ok = test_valid_nordpooldata(data, region=regions_to_verify)

if data_ok is False:
np_should_have_released_new_data = stock(func_now).replace(
hour=13, minute=RANDOM_MINUTE, second=RANDOM_SECOND
)

if type_ == "tomorrow":
if stock(func_now) >= np_should_have_released_new_data:
_LOGGER.info(
"A new data should be available, it does not exist in isnt valid"
) # retry.
return False
else:
_LOGGER.info("No new data is available")
# Need to handle the None
return None

else:
_LOGGER.info("Some crap happend, retrying request later.")
async_call_later(hass, 20, partial(self._update, type_=type_, dt=dt))
self._data[currency][type_] = data["areas"]

async def update_today(self, n: datetime):
_LOGGER.debug("Updating tomorrows prices.")
await self._update("today")
return True

attemps = []
for currency in self.currency:
update_attemp = await really_update(currency, dt)
attemps.append(update_attemp)
_LOGGER.debug("ATTEMPTS %s", attemps)

if None in attemps:
return False

return all(attemps)

async def update_tomorrow(self, n: datetime):
async def update_today(self, n: datetime, currency=None, area=None):
_LOGGER.debug("Updating todays prices.")
return await self._update("today")

async def update_tomorrow(self, n: datetime, currency=None, area=None):
_LOGGER.debug("Updating tomorrows prices.")
await self._update(type_="tomorrow", dt=dt_utils.now() + timedelta(hours=24))
self._tomorrow_valid = True
result = await self._update(
type_="tomorrow", dt=dt_utils.now() + timedelta(hours=24)
)
return result

async def _someday(self, area: str, currency: str, day: str):
"""Returns todays or tomorrows prices in a area in the currency"""
Expand All @@ -91,17 +169,14 @@ async def _someday(self, area: str, currency: str, day: str):
)

# This is needed as the currency is
# set in the sensor.
# set in the sensor and we need to pull for the first time.
if currency not in self.currency:
self.currency.append(currency)
await self.update_today(None)
await self.update_tomorrow(None)

return self._data.get(currency, {}).get(day, {}).get(area)

def tomorrow_valid(self) -> bool:
return self._tomorrow_valid

async def today(self, area: str, currency: str) -> dict:
"""Returns todays prices in a area in the requested currency"""
res = await self._someday(area, currency, "today")
Expand All @@ -123,13 +198,9 @@ async def _dry_setup(hass: HomeAssistant, config: Config) -> bool:
async def new_day_cb(n):
"""Cb to handle some house keeping when it a new day."""
_LOGGER.debug("Called new_day_cb callback")
api._tomorrow_valid = False

for curr in api.currency:
if not len(api._data[curr]["tomorrow"]):
api._data[curr]["today"] = await api.update_today(None)
else:
api._data[curr]["today"] = api._data[curr]["tomorrow"]
await api.update_today(None)
api._data[curr]["tomorrow"] = {}

async_dispatcher_send(hass, EVENT_NEW_DATA)
Expand Down Expand Up @@ -182,7 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)

# entry.add_update_listener(async_reload_entry)
entry.add_update_listener(async_reload_entry)
return res


Expand All @@ -191,9 +262,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor")

if unload_ok:
for unsub in hass.data[DOMAIN].listeners:
unsub()
hass.data.pop(DOMAIN)
if DOMAIN in hass.data:
for unsub in hass.data[DOMAIN].listeners:
unsub()
hass.data.pop(DOMAIN)

return True

Expand Down
38 changes: 29 additions & 9 deletions custom_components/nordpool/aio_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from dateutil.parser import parse as parse_dt
from nordpool.elspot import Prices

from .misc import add_junk
from .misc import add_junk, test_valid_nordpooldata

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -147,7 +147,6 @@ def __init__(self, currency, client, tz=None):
self.API_URL_CURRENCY = "https://www.nordpoolgroup.com/api/marketdata/page/%s"

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

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

Expand All @@ -168,7 +167,8 @@ async def _fetch_json(self, data_type, end_date=None, areas=None):
endDate=end_date.strftime("%d-%m-%Y"),
)

async def fetch(self, data_type, end_date=None, areas=[]):
# https://github.com/custom-components/integration_blueprint/issues/71
async def fetch(self, data_type, end_date=None, areas=None):
"""
Fetch data from API.
Inputs:
Expand All @@ -189,6 +189,8 @@ async def fetch(self, data_type, end_date=None, areas=[]):
- 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 = []

# Check how to handle all time zone in this,
# dunno how to do this yet.
Expand Down Expand Up @@ -248,26 +250,44 @@ async def fetch(self, data_type, end_date=None, areas=[]):
res = await asyncio.gather(*jobs)

raw = [self._parse_json(i, areas) for i in res]
return join_result_for_correct_time(raw, end_date)
result = join_result_for_correct_time(raw, end_date)
# test_result = test_valid_nordpooldata(result)
# _LOGGER.debug("DATA STATUS %s", test_result)
return result

async def hourly(self, end_date=None, areas=[]):
async def hourly(self, end_date=None, areas=None):
""" Helper to fetch hourly data, see Prices.fetch() """
if areas is None:
areas = []

return await self.fetch(self.HOURLY, end_date, areas)

async def daily(self, end_date=None, areas=[]):
async def daily(self, end_date=None, areas=None):
""" Helper to fetch daily data, see Prices.fetch() """
if areas is None:
areas = []

return await self.fetch(self.DAILY, end_date, areas)

async def weekly(self, end_date=None, areas=[]):
async def weekly(self, end_date=None, areas=None):
""" Helper to fetch weekly data, see Prices.fetch() """
if areas is None:
areas = []

return await self.fetch(self.WEEKLY, end_date, areas)

async def monthly(self, end_date=None, areas=[]):
async def monthly(self, end_date=None, areas=None):
""" Helper to fetch monthly data, see Prices.fetch() """
if areas is None:
areas = []

return await self.fetch(self.MONTHLY, end_date, areas)

async def yearly(self, end_date=None, areas=[]):
async def yearly(self, end_date=None, areas=None):
""" Helper to fetch yearly data, see Prices.fetch() """
if areas is None:
areas = []

return await self.fetch(self.YEARLY, end_date, areas)

def _conv_to_float(self, s):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/nordpool/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


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


Expand Down
16 changes: 11 additions & 5 deletions custom_components/nordpool/manifest.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@

{
"domain": "nordpool",
"name": "nordpool",
"documentation": "https://github.com/custom-components/nordpool/",
"issue_tracker": "https://github.com/custom-components/nordpool/issues",
"dependencies": [],
"after_dependencies": ["http"],
"after_dependencies": [
"http"
],
"config_flow": true,
"codeowners": ["@hellowlol"],
"codeowners": [
"@hellowlol"
],
"iot_class": "cloud_polling",
"requirements": ["nordpool>=0.2"],
"requirements": [
"nordpool>=0.2",
"backoff>=1.11.1"
],
"version": "0.0.4"
}
}
Loading