From c11eb5cddf0e83eb68c066c6c01c016b41915fbf Mon Sep 17 00:00:00 2001 From: Mathias Pihl Date: Wed, 1 Oct 2025 10:32:09 +0200 Subject: [PATCH] Implement quarterhourly prices --- custom_components/nordpool/__init__.py | 30 +++++++++----------- custom_components/nordpool/aio_price.py | 35 +++++++++++------------- custom_components/nordpool/const.py | 2 +- custom_components/nordpool/misc.py | 19 ------------- custom_components/nordpool/sensor.py | 18 ++++++------ custom_components/nordpool/services.py | 14 +++++----- custom_components/nordpool/services.yaml | 6 ++-- 7 files changed, 49 insertions(+), 75 deletions(-) diff --git a/custom_components/nordpool/__init__.py b/custom_components/nordpool/__init__.py index b2aca72..f5700ad 100644 --- a/custom_components/nordpool/__init__.py +++ b/custom_components/nordpool/__init__.py @@ -23,7 +23,7 @@ ISSUEURL, DOMAIN, EVENT_NEW_DAY, - EVENT_NEW_HOUR, + EVENT_NEW_QUARTERHOUR, EVENT_NEW_PRICE, _CURRENCY_LIST, RANDOM_MINUTE, @@ -73,7 +73,7 @@ async def _update(self, type_="today", dt=None, areas=None): # Keeping this for now, but this should be changed. for currency in self.currency: spot = AioPrices(currency, client) - data = await spot.hourly( + data = await spot.quarterhourly( end_date=dt, areas=self.areas if len(self.areas) > 0 else None ) if data: @@ -122,7 +122,7 @@ async def _someday(self, area: str, currency: str, day: str): # 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) + async_dispatcher_send(self._hass, EVENT_NEW_QUARTERHOUR) return self._data.get(currency, {}).get(day, {}).get(area) @@ -156,10 +156,10 @@ async def new_day_cb(_): async_dispatcher_send(hass, EVENT_NEW_DAY) - async def new_hr(_): + async def new_quarterhr(_): """Callback to tell the sensors to update on a new hour.""" - _LOGGER.debug("Called new_hr callback") - async_dispatcher_send(hass, EVENT_NEW_HOUR) + _LOGGER.debug("Called new_quarterhr callback") + async_dispatcher_send(hass, EVENT_NEW_QUARTERHOUR) @backoff.on_exception( backoff.constant, @@ -178,24 +178,20 @@ async def new_data_cb(_): async_dispatcher_send(hass, EVENT_NEW_PRICE) # Handles futures updates - cb_update_tomorrow = async_track_time_change_in_tz( + api.listeners.append(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) + api.listeners.append(async_track_time_change(hass, new_day_cb, hour=0, minute=0, second=0)) + api.listeners.append(async_track_time_change(hass, new_quarterhr, minute=0, second=0)) + api.listeners.append(async_track_time_change(hass, new_quarterhr, minute=15, second=0)) + api.listeners.append(async_track_time_change(hass, new_quarterhr, minute=30, second=0)) + api.listeners.append(async_track_time_change(hass, new_quarterhr, minute=45, second=0)) return True diff --git a/custom_components/nordpool/aio_price.py b/custom_components/nordpool/aio_price.py index 7f17704..bad552a 100644 --- a/custom_components/nordpool/aio_price.py +++ b/custom_components/nordpool/aio_price.py @@ -75,7 +75,7 @@ async def join_result_for_correct_time(results, dt): 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", + "Period has the same start and end, most likly due to dst change %s exluded this hour", val, ) elif val["value"] in INVALID_VALUES: @@ -95,19 +95,17 @@ def __init__(self, currency, client, timeezone=None): # super().__init__(currency) self.client = client self.timeezone = timeezone - (self.HOURLY, self.DAILY, self.WEEKLY, self.MONTHLY, self.YEARLY) = ( - "DayAheadPrices", - "AggregatePrices", - "AggregatePrices", - "AggregatePrices", - "AggregatePrices/GetAnnuals", - ) + self.QUARTERHOURLY = "DayAheadPrices" + self.DAILY = "AggregatePrices" + self.WEEKLY = "AggregatePrices" + self.MONTHLY = "AggregatePrices" + self.YEARLY = "AggregatePrices/GetAnnuals" self.API_URL = "https://dataportal-api.nordpoolgroup.com/api/%s" self.currency = currency async def _io(self, url, **kwargs): resp = await self.client.get(url, params=kwargs) - _LOGGER.debug("requested %s %s", resp.url, kwargs) + _LOGGER.debug("requested %s %s %d", resp.url, kwargs, resp.status) if resp.status == 204: return None @@ -143,7 +141,7 @@ def _parse_json(self, data, areas=None, data_type=None): _LOGGER.debug("data type in _parser %s, areas %s", data_type, areas) # Ripped from Kipe's nordpool - if data_type == self.HOURLY: + if data_type == self.QUARTERHOURLY: data_source = ("multiAreaEntries", "entryPerArea") elif data_type == self.DAILY: data_source = ("multiAreaDailyAggregates", "averagePerArea") @@ -217,7 +215,7 @@ 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 data_type is None: - data_type = self.HOURLY + data_type = self.QUARTERHOURLY if areas is None or len(areas) == 0: raise Exception("Cannot query with empty areas") @@ -234,12 +232,11 @@ async def _fetch_json(self, data_type, end_date=None, areas=None): "currency": self.currency, "market": "DayAhead", "deliveryArea": ",".join(areas), - # This one is default for hourly.. - "date": end_date.strftime("%Y-%m-%d"), } - if data_type != self.HOURLY: - kws.pop("date") + if data_type == self.QUARTERHOURLY: + kws["date"] = end_date.strftime("%Y-%m-%d") + else: kws["year"] = end_date.strftime("%Y") return await self._io(self.API_URL % data_type, **kws) @@ -254,7 +251,7 @@ async def fetch(self, data_type, end_date=None, areas=None, raw=False): Fetch data from API. Inputs: - data_type - API page id, one of Prices.HOURLY, Prices.DAILY etc + API page id, one of Prices.QUARTERHOURLY, Prices.DAILY etc - end_date datetime to end the data fetching defaults to tomorrow @@ -283,7 +280,7 @@ async def fetch(self, data_type, end_date=None, areas=None, raw=False): yesterday = today - timedelta(days=1) tomorrow = today + timedelta(days=1) - if data_type == self.HOURLY: + if data_type == self.QUARTERHOURLY: if raw: return await self._fetch_json(data_type, today, areas) jobs = [ @@ -314,11 +311,11 @@ async def _async_parse_json(self, data, areas, data_type): None, self._parse_json, data, areas, data_type ) - async def hourly(self, end_date=None, areas=None, raw=False): + async def quarterhourly(self, end_date=None, areas=None, raw=False): """Helper to fetch hourly data, see Prices.fetch()""" if areas is None: areas = [] - return await self.fetch(self.HOURLY, end_date, areas, raw=raw) + return await self.fetch(self.QUARTERHOURLY, end_date, areas, raw=raw) async def daily(self, end_date=None, areas=None): """Helper to fetch daily data, see Prices.fetch()""" diff --git a/custom_components/nordpool/const.py b/custom_components/nordpool/const.py index 1ef7f8d..b2d476b 100644 --- a/custom_components/nordpool/const.py +++ b/custom_components/nordpool/const.py @@ -4,7 +4,7 @@ DOMAIN = "nordpool" RANDOM_MINUTE = randint(10, 30) RANDOM_SECOND = randint(0, 59) -EVENT_NEW_HOUR = "nordpool_update_hour" +EVENT_NEW_QUARTERHOUR = "nordpool_update_quarterhour" EVENT_NEW_DAY = "nordpool_update_day" EVENT_NEW_PRICE = "nordpool_update_new_price" SENTINEL = object() diff --git a/custom_components/nordpool/misc.py b/custom_components/nordpool/misc.py index 864ffe9..b6bbf34 100644 --- a/custom_components/nordpool/misc.py +++ b/custom_components/nordpool/misc.py @@ -14,8 +14,6 @@ "is_new", "has_junk", "extract_attrs", - "start_of", - "end_of", "stock", "add_junk", ] @@ -54,14 +52,6 @@ def stock(d): """convert datetime to stocholm time.""" return d.astimezone(stockholm_tz) - -def start_of(d, typ_="hour"): - if typ_ == "hour": - return d.replace(minute=0, second=0, microsecond=0) - elif typ_ == "day": - return d.replace(hour=0, minute=0, microsecond=0) - - def time_in_range(start, end, x): """Return true if x is in the range [start, end]""" if start <= end: @@ -69,15 +59,6 @@ def time_in_range(start, end, x): else: return start <= x or x <= end - -def end_of(d, typ_="hour"): - """Return end our hour""" - if typ_ == "hour": - return d.replace(minute=59, second=59, microsecond=999999) - elif typ_ == "day": - return d.replace(hour=23, minute=59, second=59, microsecond=999999) - - def is_new(date=None, typ="day") -> bool: """Utility to check if its a new hour or day.""" # current = pendulum.now() diff --git a/custom_components/nordpool/sensor.py b/custom_components/nordpool/sensor.py index aad5f03..0c93869 100644 --- a/custom_components/nordpool/sensor.py +++ b/custom_components/nordpool/sensor.py @@ -24,7 +24,7 @@ DOMAIN, EVENT_NEW_DAY, EVENT_NEW_PRICE, - EVENT_NEW_HOUR, + EVENT_NEW_QUARTERHOUR, SENTINEL, RANDOM_MINUTE, RANDOM_SECOND, @@ -35,7 +35,7 @@ _CURRENTY_TO_CENTS, _CENT_MULTIPLIER, ) -from .misc import start_of, stock +from .misc import time_in_range, stock _LOGGER = logging.getLogger(__name__) @@ -429,7 +429,7 @@ async def _update_current_price(self) -> None: data = await self._api.today(self._area, self._currency) if data: for item in self._someday(data): - if item["start"] == start_of(local_now, "hour"): + if time_in_range(item["start"], item["end"], local_now): self._current_price = item["value"] _LOGGER.debug( "Updated %s _current_price %s", self.name, item["value"] @@ -442,11 +442,11 @@ async def handle_new_day(self): _LOGGER.debug("handle_new_day") self._data_tomorrow = None # update attrs for the new day - await self.handle_new_hr() + await self.handle_new_quarterhr() - async def handle_new_hr(self): + async def handle_new_quarterhr(self): """Update attrs for the new hour""" - _LOGGER.debug("handle_new_hr") + _LOGGER.debug("handle_new_quarterhr") today = await self._api.today(self._area, self._currency) if today: self._data_today = today @@ -473,7 +473,7 @@ async def handle_new_price(self): if tomorrow: self._data_tomorrow = tomorrow - await self.handle_new_hr() + await self.handle_new_quarterhr() async def async_added_to_hass(self): """Connect to dispatcher listening for entity data notifications.""" @@ -484,5 +484,5 @@ async def async_added_to_hass(self): async_dispatcher_connect( self._api._hass, EVENT_NEW_PRICE, self.handle_new_price ) - async_dispatcher_connect(self._api._hass, EVENT_NEW_HOUR, self.handle_new_hr) - await self.handle_new_hr() + async_dispatcher_connect(self._api._hass, EVENT_NEW_QUARTERHOUR, self.handle_new_quarterhr) + await self.handle_new_quarterhr() diff --git a/custom_components/nordpool/services.py b/custom_components/nordpool/services.py index 5cd3d8a..7f82d44 100644 --- a/custom_components/nordpool/services.py +++ b/custom_components/nordpool/services.py @@ -28,7 +28,7 @@ def validator(value): return validator -HOURLY_SCHEMA = vol.Schema( +QUARTERHOURLY_SCHEMA = vol.Schema( { vol.Required("currency"): str, vol.Required("date"): cv.date, @@ -54,16 +54,16 @@ async def async_setup_services(hass: HomeAssistant): client = async_get_clientsession(hass) - async def hourly(service_call: ServiceCall) -> Any: + async def quarterhourly(service_call: ServiceCall) -> Any: sc = service_call.data - _LOGGER.debug("called hourly with %r", sc) + _LOGGER.debug("called quarterhourly with %r", sc) # Convert the date to datetime as the rest of the code expects a datetime. We will want to keep date as it easier for ppl to use. end_date = datetime( year=sc["date"].year, month=sc["date"].month, day=sc["date"].day ) - value = await AioPrices(sc["currency"], client).hourly( + value = await AioPrices(sc["currency"], client).quarterhourly( areas=sc["area"], end_date=end_date, raw=True ) @@ -114,9 +114,9 @@ async def daily(service_call: ServiceCall): hass.services.async_register( domain="nordpool", - service="hourly", - service_func=hourly, - schema=HOURLY_SCHEMA, + service="quarterhourly", + service_func=quarterhourly, + schema=QUARTERHOURLY_SCHEMA, supports_response=SupportsResponse.OPTIONAL, ) hass.services.async_register( diff --git a/custom_components/nordpool/services.yaml b/custom_components/nordpool/services.yaml index 87e374a..e86d996 100644 --- a/custom_components/nordpool/services.yaml +++ b/custom_components/nordpool/services.yaml @@ -44,10 +44,10 @@ monthly: description: "Return the prices for what price area" example: "NO2" -hourly: - name: hourly +quarterhourly: + name: quarterhourly description: >- - Action that gets the raw hourly price for spesific date from Nordpool + Action that gets the raw quarterhourly price for spesific date from Nordpool fields: currency: description: "What currecy should the prices be returned in"