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 1 commit
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
103 changes: 78 additions & 25 deletions custom_components/nordpool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
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 +24,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 Down Expand Up @@ -52,7 +80,7 @@ def __init__(self, hass: HomeAssistant):
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)
Expand All @@ -64,23 +92,54 @@ async def _update(self, type_="today", dt=None):
# when the region is in another timezone
# as we request data for 3 days anyway.
# Keeping this for now, but this should be changed.
attemps = []
for currency in self.currency:
spot = AioPrices(currency, client)
data = await spot.hourly(end_date=dt)
if data:
# 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)
attemps.append(data_ok)
if data_ok is False:
np_should_have_released_new_data = stock(dt).replace(
hour=13, minute=RANDOM_MINUTE, second=RANDOM_SECOND
)

if type_ == "tomorrow":
if stock(dt) >= np_should_have_released_new_data:
_LOGGER.info(
"The time is %s, but nordpool havnt released any new data retrying in 5 minutes",
dt,
)
p = partial(self._update, type_, dt)
async_call_later(hass, 60 * 5, p)
else:
_LOGGER.debug("No new data is availble yet")

else:
_LOGGER.debug("Retrying request for %s", type_)
p = partial(self._update, type_, dt)
async_call_later(hass, 60 * 5, p)
Hellowlol marked this conversation as resolved.
Show resolved Hide resolved
return False

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

async def update_today(self, n: datetime):
_LOGGER.debug("Updating tomorrows prices.")
await self._update("today")
_LOGGER.debug("ATTEMPTS %s", attemps)
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 @@ -99,9 +158,6 @@ async def _someday(self, area: str, currency: str, day: str):

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 +179,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 +234,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 +243,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
41 changes: 32 additions & 9 deletions custom_components/nordpool/aio_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from dateutil import tz
from dateutil.parser import parse as parse_dt
from nordpool.elspot import Prices
import backoff
import aiohttp

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

_LOGGER = logging.getLogger(__name__)

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

@backoff.on_exception(backoff.expo, aiohttp.ClientError, logger=_LOGGER)
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 +170,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 +192,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 +253,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"
}
}
55 changes: 42 additions & 13 deletions custom_components/nordpool/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"end_of",
"stock",
"add_junk",
"test_valid_nordpooldata",
]

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,6 +81,47 @@ def is_inf(d):
return False


def test_valid_nordpolldata2(data_, region=None):
# not in use atm
for key, value in data.get("areas", {}).items():
if region is None or key in region:
# if region is not None and area in region:
if any([i["value"] == float("inf") for i in value.get("values", {})]):
_LOGGER.debug("Found infinty invalid data in %s", key)

return False

return True


def test_valid_nordpooldata(data_, region=None):
# from pprint import pformat

_LOGGER.debug("Checking for inf value in data for %s", region)

# _LOGGER.debug("DATA %s", pformat(data_))
if isinstance(data_, dict):
data_ = [data_]

for data in data_:
for currency, v in data.items():
for area, real_data in v.items():
# _LOGGER.debug("area %s", area)
if region is None or area in region:
# if region is not None and area in region:
if any(
[
i["value"] == float("inf")
for i in real_data.get("values", {})
]
):
_LOGGER.debug("Found infinty invalid data in area %s", area)

return False

return True


def has_junk(data) -> bool:
"""Check if data has some infinity values.

Expand Down Expand Up @@ -116,16 +158,3 @@ def extract_attrs(data) -> dict:
return d

return data


'''
def as_tz(dattim, tz=None):
"""Convert a UTC datetime object to local time zone."""

if dattim.tzinfo is None:
dattim = UTC.localize(dattim)

return dattim.astimezone(timezone(tz))


'''
Loading