Files
HomeAssistant/custom_components/nordpool/__init__.py
T
2025-09-11 10:47:34 +03:00

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)