239 lines
8.1 KiB
Python
239 lines
8.1 KiB
Python
import logging
|
|
from collections import defaultdict
|
|
from datetime import timedelta
|
|
|
|
|
|
import backoff
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.const import Platform
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.event import async_track_time_change
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.util import dt as dt_utils
|
|
|
|
from .aio_price import AioPrices, InvalidValueException
|
|
from .events import async_track_time_change_in_tz
|
|
from .services import async_setup_services
|
|
|
|
from .const import (
|
|
NAME,
|
|
VERSION,
|
|
ISSUEURL,
|
|
DOMAIN,
|
|
EVENT_NEW_DAY,
|
|
EVENT_NEW_HOUR,
|
|
EVENT_NEW_PRICE,
|
|
_CURRENCY_LIST,
|
|
RANDOM_MINUTE,
|
|
RANDOM_SECOND,
|
|
)
|
|
|
|
|
|
STARTUP = f"""
|
|
-------------------------------------------------------------------
|
|
{NAME}
|
|
Version: {VERSION}
|
|
This is a custom component
|
|
If you have any issues with this you need to open an issue here:
|
|
{ISSUEURL}
|
|
-------------------------------------------------------------------
|
|
"""
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
|
|
|
|
|
class NordpoolData:
|
|
"""Holds the data"""
|
|
|
|
def __init__(self, hass: HomeAssistant):
|
|
self._hass = hass
|
|
self._last_tick = None
|
|
self._data = defaultdict(dict)
|
|
self.currency = []
|
|
self.listeners = []
|
|
self.areas = []
|
|
|
|
async def _update(self, type_="today", dt=None, areas=None):
|
|
_LOGGER.debug("calling _update %s %s %s", type_, dt, areas)
|
|
hass = self._hass
|
|
client = async_get_clientsession(hass)
|
|
|
|
if dt is None:
|
|
dt = dt_utils.now()
|
|
|
|
if areas is not None:
|
|
self.areas += [area for area in areas if area not in self.areas]
|
|
# 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:
|
|
spot = AioPrices(currency, client)
|
|
data = await spot.hourly(
|
|
end_date=dt, areas=self.areas if len(self.areas) > 0 else None
|
|
)
|
|
if data:
|
|
self._data[currency][type_] = data["areas"]
|
|
|
|
async def update_today(self, areas=None):
|
|
"""Update today's prices"""
|
|
_LOGGER.debug("Updating today's prices.")
|
|
if areas is not None:
|
|
self.areas += [area for area in areas if area not in self.areas]
|
|
await self._update("today", areas=self.areas if len(self.areas) > 0 else None)
|
|
|
|
async def update_tomorrow(self, areas=None):
|
|
"""Update tomorrows prices."""
|
|
_LOGGER.debug("Updating tomorrows prices.")
|
|
if areas is not None:
|
|
self.areas += [area for area in areas if area not in self.areas]
|
|
await self._update(
|
|
type_="tomorrow",
|
|
dt=dt_utils.now() + timedelta(hours=24),
|
|
areas=self.areas if len(self.areas) > 0 else None,
|
|
)
|
|
|
|
async def _someday(self, area: str, currency: str, day: str):
|
|
"""Returns today's or tomorrow's prices in an area in the currency"""
|
|
if currency not in _CURRENCY_LIST:
|
|
raise ValueError(
|
|
"%s is an invalid currency, possible values are %s"
|
|
% (currency, ", ".join(_CURRENCY_LIST))
|
|
)
|
|
|
|
if area not in self.areas:
|
|
self.areas.append(area)
|
|
# This is needed as the currency is
|
|
# set in the sensor.
|
|
if currency not in self.currency:
|
|
self.currency.append(currency)
|
|
try:
|
|
await self.update_today(areas=self.areas)
|
|
except InvalidValueException:
|
|
_LOGGER.debug("No data available for today, retrying later")
|
|
try:
|
|
await self.update_tomorrow(areas=self.areas)
|
|
except InvalidValueException:
|
|
_LOGGER.debug("No data available for tomorrow, retrying later")
|
|
|
|
# Send a new data request after new data is updated for this first run
|
|
# This way if the user has multiple sensors they will all update
|
|
async_dispatcher_send(self._hass, EVENT_NEW_HOUR)
|
|
|
|
return self._data.get(currency, {}).get(day, {}).get(area)
|
|
|
|
async def today(self, area: str, currency: str) -> dict:
|
|
"""Returns today's prices in an area in the requested currency"""
|
|
return await self._someday(area, currency, "today")
|
|
|
|
async def tomorrow(self, area: str, currency: str):
|
|
"""Returns tomorrow's prices in an area in the requested currency"""
|
|
return await self._someday(area, currency, "tomorrow")
|
|
|
|
|
|
async def _dry_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up using yaml config file."""
|
|
if DOMAIN not in hass.data:
|
|
api = NordpoolData(hass)
|
|
hass.data[DOMAIN] = api
|
|
_LOGGER.debug("Added %s to hass.data", DOMAIN)
|
|
await async_setup_services(hass)
|
|
|
|
async def new_day_cb(_):
|
|
"""Cb to handle some house keeping when it a new day."""
|
|
_LOGGER.debug("Called new_day_cb callback")
|
|
|
|
for curr in api.currency:
|
|
if not api._data.get(curr, {}).get("tomorrow"):
|
|
api._data[curr]["today"] = await api.update_today()
|
|
else:
|
|
api._data[curr]["today"] = api._data[curr]["tomorrow"]
|
|
api._data[curr]["tomorrow"] = {}
|
|
|
|
async_dispatcher_send(hass, EVENT_NEW_DAY)
|
|
|
|
async def new_hr(_):
|
|
"""Callback to tell the sensors to update on a new hour."""
|
|
_LOGGER.debug("Called new_hr callback")
|
|
async_dispatcher_send(hass, EVENT_NEW_HOUR)
|
|
|
|
@backoff.on_exception(
|
|
backoff.constant,
|
|
(InvalidValueException),
|
|
logger=_LOGGER,
|
|
interval=600,
|
|
max_time=7200,
|
|
jitter=None,
|
|
)
|
|
async def new_data_cb(_):
|
|
"""Callback to fetch new data for tomorrows prices at 1300ish CET
|
|
and notify any sensors, about the new data
|
|
"""
|
|
# _LOGGER.debug("Called new_data_cb")
|
|
await api.update_tomorrow()
|
|
async_dispatcher_send(hass, EVENT_NEW_PRICE)
|
|
|
|
# Handles futures updates
|
|
cb_update_tomorrow = async_track_time_change_in_tz(
|
|
hass,
|
|
new_data_cb,
|
|
hour=13,
|
|
minute=RANDOM_MINUTE,
|
|
second=RANDOM_SECOND,
|
|
tz=await dt_utils.async_get_time_zone("Europe/Stockholm"),
|
|
)
|
|
|
|
cb_new_day = async_track_time_change(
|
|
hass, new_day_cb, hour=0, minute=0, second=0
|
|
)
|
|
|
|
cb_new_hr = async_track_time_change(hass, new_hr, minute=0, second=0)
|
|
|
|
api.listeners.append(cb_update_tomorrow)
|
|
api.listeners.append(cb_new_hr)
|
|
api.listeners.append(cb_new_day)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up using yaml config file."""
|
|
return await _dry_setup(hass, config)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up nordpool as config entry."""
|
|
res = await _dry_setup(hass, entry.data)
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
|
|
entry.add_update_listener(async_reload_entry)
|
|
return res
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
if unload_ok:
|
|
# This is an issue if you have multiple sensors as everything related to DOMAIN
|
|
# is removed, regardless if you have multiple sensors or not. Doesn't seem to
|
|
# create a big issue for now #TODO
|
|
if DOMAIN in hass.data:
|
|
for unsub in hass.data[DOMAIN].listeners:
|
|
unsub()
|
|
hass.data.pop(DOMAIN)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
"""Reload config entry."""
|
|
await async_unload_entry(hass, entry)
|
|
await async_setup_entry(hass, entry)
|