diff --git a/README.md b/README.md index 350fbe4..8668340 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,15 @@ email address you entered during registration in the email body. ### Sensors The integration adds the following sensors: -- Average Day-Ahead Electricity Price Today (This integration carries attributes with all prices) -- Highest Day-Ahead Electricity Price Today -- Lowest Day-Ahead Electricity Price Today -- Current Day-Ahead Electricity Price -- Current Percentage Relative To Highest Electricity Price Of The Day -- Next Hour Day-Ahead Electricity Price +- Current Electricity Price +- Next Hour Electricity Price + +And some price analysis sensors: +- Average Day-Ahead Electricity Price (This integration carries attributes with all prices) +- Current Percentage Relative To Highest Electricity Price +- Current Percentage Relative To Spread Electricity Price +- Highest Day-Ahead Electricity Price +- Lowest Day-Ahead Electricity Price - Time Of Highest Energy Price Today - Time Of Lowest Energy Price Today @@ -79,23 +82,46 @@ An example template is given below. You can find and share other templates [here {% endif %} {% endif %} ``` -### Calculation method -This changes the calculated (min,max,avg values) entities behaviour to one of: - -- Sliding -The min/max/etc entities will get updated every hour with only upcoming data. -This means that the min price returned at 13:00 will be the lowest price in the future (as available from that point in time). -Regardless of past hours that might have had a lower price (this is most useful if you want to be able to schedule loads as soon and cheap as possible) - -- Default (on publish) -The min/max/etc entities will get updated once new data becomes available. -This means that the min price will update once the next days pricing becomes available (usually between 12:00 and 15:00) -It also means that until the next days pricing becomes available the latest 48h of available data will be used to calculate a min price - -- Rotation -The min/max/etc entities will get updated at midnight. -This means that the min price returned at 23:59 will be based on the day x price while at 00:00 the day x+1 price will be the only one used in the calculations) -day x in this case is a random date like 2022-10-10 and day x+1 2022-10-11 +### Analysis Window (previously called Calculation method) +The analysis window defines which period to use for calculating the min,max,avg & perc values. + +![image](https://github.com/user-attachments/assets/c7978e26-1fa9-417b-9e2f-830f8b4ccd1f) + +The analysis window can be set to: + +- Publish (Default) + +The min/max/etc entities will get updated once new data becomes available (usualy between 12:00 and 15:00) +It also means that until the next days pricing becomes available the analysis is performed on the latest 48h of available data (yesterday and today) + +- Today + +The analysis is performed on todays data. Sensor data will be updated at midnight + +- Sliding-12 + +An analysis window of 12 hours which moves along with the changing hour. Meaning the analysis sensors change each hour. +The window starts 6-hours before tha last hour and ends 6 hrs after. So its using a 12 hour window to detect half-day low-/high price periods + +- Sliding-24 + +Same as above but using a 24 hour sliding analysis window + +- Forward-12 + +Same 12 hours sliding window, however starting from the last hour upto 12 hours beyond. Usefull to detect half-day min/max values whih occur in the future. + +Note that because the sensors are updated each hour, the values may change just before you would expect a trigger to be fired. For example the timestamp of the minimum price may change to a later date when the analysis window shifts one hour and by this got another lower minimum price, included in the dataset. This situation may continue while lower prices keep on turning up in future hours while shifting the window. It may however help you to charge your EV at the lowest price in the comming days + +- Forward-24 + +Same as above but using a 24 hour window. + +Depricated +- Sliding. Please use 'forward-24' + +- Rotation. Please use 'Today' + ### ApexChart Graph diff --git a/custom_components/entsoe/__init__.py b/custom_components/entsoe/__init__.py index 07477b7..9d513d4 100644 --- a/custom_components/entsoe/__init__.py +++ b/custom_components/entsoe/__init__.py @@ -10,11 +10,11 @@ from homeassistant.helpers.typing import ConfigType from .const import ( - CALCULATION_MODE, + ANALYSIS_WINDOW, CONF_API_KEY, CONF_AREA, CONF_ENERGY_SCALE, - CONF_CALCULATION_MODE, + CONF_ANALYSIS_WINDOW, CONF_MODIFYER, CONF_VAT_VALUE, DEFAULT_MODIFYER, @@ -45,8 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: energy_scale = entry.options.get(CONF_ENERGY_SCALE, DEFAULT_ENERGY_SCALE) modifyer = entry.options.get(CONF_MODIFYER, DEFAULT_MODIFYER) vat = entry.options.get(CONF_VAT_VALUE, 0) - calculation_mode = entry.options.get( - CONF_CALCULATION_MODE, CALCULATION_MODE["default"] + analysis_window = entry.options.get( + CONF_ANALYSIS_WINDOW, ANALYSIS_WINDOW["default"] ) entsoe_coordinator = EntsoeCoordinator( hass, @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: area=area, energy_scale=energy_scale, modifyer=modifyer, - calculation_mode=calculation_mode, + analysis_window=analysis_window, VAT=vat, ) diff --git a/custom_components/entsoe/config_flow.py b/custom_components/entsoe/config_flow.py index 318a002..24c54a5 100644 --- a/custom_components/entsoe/config_flow.py +++ b/custom_components/entsoe/config_flow.py @@ -21,12 +21,12 @@ from .const import ( AREA_INFO, - CALCULATION_MODE, + ANALYSIS_WINDOW, COMPONENT_TITLE, CONF_ADVANCED_OPTIONS, CONF_API_KEY, CONF_AREA, - CONF_CALCULATION_MODE, + CONF_ANALYSIS_WINDOW, CONF_CURRENCY, CONF_ENERGY_SCALE, CONF_ENTITY_NAME, @@ -94,7 +94,7 @@ async def async_step_user( user_input[CONF_MODIFYER] = DEFAULT_MODIFYER user_input[CONF_CURRENCY] = DEFAULT_CURRENCY user_input[CONF_ENERGY_SCALE] = DEFAULT_ENERGY_SCALE - user_input[CONF_CALCULATION_MODE] = CALCULATION_MODE["default"] + user_input[CONF_ANALYSIS_WINDOW] = ANALYSIS_WINDOW["default"] return self.async_create_entry( title=self.name or COMPONENT_TITLE, @@ -108,7 +108,7 @@ async def async_step_user( CONF_ADVANCED_OPTIONS: user_input[CONF_ADVANCED_OPTIONS], CONF_VAT_VALUE: user_input[CONF_VAT_VALUE], CONF_ENTITY_NAME: user_input[CONF_ENTITY_NAME], - CONF_CALCULATION_MODE: user_input[CONF_CALCULATION_MODE], + CONF_ANALYSIS_WINDOW: user_input[CONF_ANALYSIS_WINDOW], }, ) @@ -185,8 +185,8 @@ async def async_step_extra(self, user_input=None): CONF_ENERGY_SCALE: user_input[CONF_ENERGY_SCALE], CONF_VAT_VALUE: user_input[CONF_VAT_VALUE], CONF_ENTITY_NAME: user_input[CONF_ENTITY_NAME], - CONF_CALCULATION_MODE: user_input[ - CONF_CALCULATION_MODE + CONF_ANALYSIS_WINDOW: user_input[ + CONF_ANALYSIS_WINDOW ], }, ) @@ -212,12 +212,12 @@ async def async_step_extra(self, user_input=None): CONF_ENERGY_SCALE, default=DEFAULT_ENERGY_SCALE ): vol.In(list(ENERGY_SCALES.keys())), vol.Optional( - CONF_CALCULATION_MODE, default=CALCULATION_MODE["default"] + CONF_ANALYSIS_WINDOW, default=ANALYSIS_WINDOW["default"] ): SelectSelector( SelectSelectorConfig( options=[ SelectOptionDict(value=value, label=key) - for key, value in CALCULATION_MODE.items() + for key, value in ANALYSIS_WINDOW.items() if key != "default" ] ), @@ -286,7 +286,7 @@ async def async_step_init( errors["base"] = "invalid_template" calculation_mode_default = self.config_entry.options.get( - CONF_CALCULATION_MODE, CALCULATION_MODE["default"] + CONF_ANALYSIS_WINDOW, ANALYSIS_WINDOW["default"] ) return self.async_show_form( @@ -328,13 +328,13 @@ async def async_step_init( ), ): vol.In(list(ENERGY_SCALES.keys())), vol.Optional( - CONF_CALCULATION_MODE, + CONF_ANALYSIS_WINDOW, default=calculation_mode_default, ): SelectSelector( SelectSelectorConfig( options=[ SelectOptionDict(value=value, label=key) - for key, value in CALCULATION_MODE.items() + for key, value in ANALYSIS_WINDOW.items() if key != "default" ] ), diff --git a/custom_components/entsoe/const.py b/custom_components/entsoe/const.py index 99f1e4c..53f9a1a 100644 --- a/custom_components/entsoe/const.py +++ b/custom_components/entsoe/const.py @@ -12,7 +12,7 @@ CONF_CURRENCY = "currency" CONF_ENERGY_SCALE = "energy_scale" CONF_ADVANCED_OPTIONS = "advanced_options" -CONF_CALCULATION_MODE = "calculation_mode" +CONF_ANALYSIS_WINDOW = "analysis_window" CONF_VAT_VALUE = "VAT_value" DEFAULT_MODIFYER = "{{current_price}}" @@ -20,11 +20,14 @@ DEFAULT_ENERGY_SCALE = "kWh" # default is only for internal use / backwards compatibility -CALCULATION_MODE = { - "default": "publish", - "rotation": "rotation", - "sliding": "sliding", - "publish": "publish", +ANALYSIS_WINDOW = { + "default": "publish", + "publish": "publish", + "today": "today", + "sliding-24": "sliding-24", + "sliding-12": "sliding-12", # new half day sliding + "forward-24": "forward-24", # 24hrs forward looking + "forward-12": "forward-12", # 12hrs forward looking } ENERGY_SCALES = { "kWh": 1000, "MWh": 1 } diff --git a/custom_components/entsoe/coordinator.py b/custom_components/entsoe/coordinator.py index 6d464c6..305a2e9 100644 --- a/custom_components/entsoe/coordinator.py +++ b/custom_components/entsoe/coordinator.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import datetime, timedelta +from datetime import timedelta import homeassistant.helpers.config_validation as cv from homeassistant.core import HomeAssistant @@ -12,12 +12,16 @@ from requests.exceptions import HTTPError from .api_client import EntsoeClient -from .const import AREA_INFO, CALCULATION_MODE, DEFAULT_MODIFYER, ENERGY_SCALES +from .const import AREA_INFO, ANALYSIS_WINDOW, DEFAULT_MODIFYER, ENERGY_SCALES -# depending on timezone les than 24 hours could be returned. +# depending on timezone less than 24 hours could be returned. +# is this still a valid minimum now that we fill missing hours in the api_client? MIN_HOURS = 20 +# This class contains actually two main tasks +# 1. ENTSO: Refresh data from ENTSO on interval basis triggered by HASS every 60 minutes +# 2. ANALYSIS: Implement some analysis on this data, like min(), max(), avg(), perc(). Updated analysis is triggered by an explicit call from a sensor class EntsoeCoordinator(DataUpdateCoordinator): """Get the latest data and update the states.""" @@ -28,7 +32,7 @@ def __init__( area, energy_scale, modifyer, - calculation_mode=CALCULATION_MODE["default"], + analysis_window=ANALYSIS_WINDOW["default"], VAT=0, ) -> None: """Initialize the data object.""" @@ -37,10 +41,10 @@ def __init__( self.modifyer = modifyer self.area = AREA_INFO[area]["code"] self.energy_scale = energy_scale - self.calculation_mode = calculation_mode + self.analysis_window = analysis_window self.vat = VAT self.today = None - self.calculator_last_sync = None + self.last_analysis = None self.filtered_hourprices = [] # Check incase the sensor was setup using config flow. @@ -62,7 +66,7 @@ def __init__( update_interval=timedelta(minutes=60), ) - # calculate the price using the given template + # ENTSO: recalculate the price using the given template def calc_price(self, value, fake_dt=None, no_template=False) -> float: """Calculate price based on the users settings.""" # Used to inject the current hour. @@ -90,12 +94,13 @@ def inner(*args, **kwargs): return price + # ENTSO: recalculate the price for each price def parse_hourprices(self, hourprices): for hour, price in hourprices.items(): hourprices[hour] = self.calc_price(value=price, fake_dt=hour) return hourprices - # Called by HA every refresh interval (60 minutes) + # ENTSO: Triggered by HA to refresh the data (interval = 60 minutes) async def _async_update_data(self) -> dict: """Get the latest data from ENTSO-e""" self.logger.debug("ENTSO-e DataUpdateCoordinator data update") @@ -104,7 +109,7 @@ async def _async_update_data(self) -> dict: now = dt.now() self.today = now.replace(hour=0, minute=0, second=0, microsecond=0) if self.check_update_needed(now) is False: - self.logger.debug(f"Skipping api fetch. All data is already available") + self.logger.debug("Skipping api fetch. All data is already available") return self.data yesterday = self.today - timedelta(days=1) @@ -122,9 +127,13 @@ async def _async_update_data(self) -> dict: f"received pricing data from entso-e for {len(data)} hours" ) self.data = parsed_data - self.filtered_hourprices = self._filter_calculated_hourprices(parsed_data) + self.last_analysis = ( + None # data was updated so force a refresh of the analysis + ) + self.refresh_analysis() return parsed_data + # ENTSO: check if we need to refresh the data. If we have None, or less than 20hrs for today, or less than 20hrs tomorrow and its after 11 def check_update_needed(self, now): if self.data is None: return True @@ -134,6 +143,7 @@ def check_update_needed(self, now): return True return False + # ENTSO: fetch new prices using an async job async def fetch_prices(self, start_date, end_date): try: # run api_update in async job @@ -161,143 +171,191 @@ async def fetch_prices(self, start_date, end_date): f"Warning the integration doesn't have any up to date local data this means that entities won't get updated but access remains to restorable entities: {exc}." ) + # ENTSO: the async fetch job itself def api_update(self, start_date, end_date, api_key): client = EntsoeClient(api_key=api_key) return client.query_day_ahead_prices( country_code=self.area, start=start_date, end=end_date ) - async def get_energy_prices(self, start_date, end_date): - # check if we have the data already - if ( - len(self.get_data(start_date)) > MIN_HOURS - and len(self.get_data(end_date)) > MIN_HOURS - ): - self.logger.debug(f"return prices from coordinator cache.") - return { - k: v - for k, v in self.data.items() - if k.date() >= start_date.date() and k.date() <= end_date.date() - } - return self.parse_hourprices(await self.fetch_prices(start_date, end_date)) + # -------------------------------------------------------------------------------------------------------------------------------- + # ENTSO: Return the data for the given date + def get_data(self, date): + return {k: v for k, v in self.data.items() if k.date() == date.date()} + + # ENTSO: Return the most recent 48hrs dataset + # We limit ourselves to 48 hrs as + # - On reboot between 0:00 and ~13:00 we only have 48hrs of data + # - On operations passing the 0:00 we have data of today, yesterday and the day before yesterday + # - After ~13:00 we will be back to 72hrs of yesterday, today and tomorrow + def get_48hrs_data(self): + today = self.get_data_today() + tomorrow = self.get_data_tomorrow() + + if len(tomorrow) < MIN_HOURS: + yesterday = self.get_data_yesterday() + return {**yesterday, **today} + + return {**today, **tomorrow} + + # ENTSO: Return the data for today + def get_data_today(self): + return self.get_data(self.today) + + # ENTSO: Return the data for tomorrow + def get_data_tomorrow(self): + return self.get_data(self.today + timedelta(days=1)) + + # ENTSO: Return the data for yesterday + def get_data_yesterday(self): + return self.get_data(self.today - timedelta(days=1)) + + # -------------------------------------------------------------------------------------------------------------------------------- + # SENSOR: Get the current price + def get_current_hourprice(self) -> int: + return self.data[dt.now().replace(minute=0, second=0, microsecond=0)] - def today_data_available(self): - return len(self.get_data_today()) > MIN_HOURS + # SENSOR: Get the next hour price + def get_next_hourprice(self) -> int: + return self.data[ + dt.now().replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + ] + + # SENSOR: Get timestamped prices of today + def get_prices_today(self): + return self.get_timestamped_prices(self.get_data_today()) + + # SENSOR: Get timestamped prices of tomorrow + def get_prices_tomorrow(self): + return self.get_timestamped_prices(self.get_data_tomorrow()) + + # SENSOR: Get timestamped prices of yesterday + def get_prices_yesterday(self): + return self.get_timestamped_prices(self.get_data_yesterday()) + + # SENSOR: Get timestamped 48hrs prices + def get_prices(self): + return self.get_timestamped_prices(self.get_48hrs_data()) - # this method is called by each sensor, each complete hour, and ensures the date and filtered hourprices are in line with the current time + # SENSOR: Helper to timestamp the prices + def get_timestamped_prices(self, hourprices): + list = [] + for hour, price in hourprices.items(): + str_hour = str(hour) + list.append({"time": str_hour, "price": price}) + return list + + # -------------------------------------------------------------------------------------------------------------------------------- + # ANALYSIS: this method is called by each sensor, each complete hour, and ensures the date and filtered hourprices are in line with the current time # we could still optimize as not every calculator mode needs hourly updates - def sync_calculator(self): + def refresh_analysis(self): now = dt.now() - if ( - self.calculator_last_sync is None - or self.calculator_last_sync.hour != now.hour - ): + if self.last_analysis is None or self.last_analysis.hour != now.hour: self.logger.debug( - f"The calculator needs to be synced with the current time" + "The analysis window needs to be updated to the current time" ) if self.today.date() != now.date(): self.logger.debug( - f"new day detected: update today and filtered hourprices" + "new day detected: update today and filtered hourprices" ) self.today = now.replace(hour=0, minute=0, second=0, microsecond=0) - self.filtered_hourprices = self._filter_calculated_hourprices(self.data) - - self.calculator_last_sync = now - - def _filter_calculated_hourprices(self, data): - # rotation = calculations made upon 24hrs today - if self.calculation_mode == CALCULATION_MODE["rotation"]: - return { - hour: price - for hour, price in data.items() - if hour >= self.today and hour < self.today + timedelta(days=1) - } - # sliding = calculations made on all data from the current hour and beyond (future data only) - elif self.calculation_mode == CALCULATION_MODE["sliding"]: - now = dt.now().replace(minute=0, second=0, microsecond=0) - return {hour: price for hour, price in data.items() if hour >= now} - # publish >48 hrs of data = calculations made on all data of today and tomorrow (48 hrs) - elif self.calculation_mode == CALCULATION_MODE["publish"] and len(data) > 48: - return {hour: price for hour, price in data.items() if hour >= self.today} - # publish <=48 hrs of data = calculations made on all data of yesterday and today (48 hrs) - elif self.calculation_mode == CALCULATION_MODE["publish"]: - return { - hour: price - for hour, price in data.items() - if hour >= self.today - timedelta(days=1) - } + self.filtered_hourprices = self._filter_analysis_window(self.data) - def get_prices_today(self): - return self.get_timestamped_prices(self.get_data_today()) + self.last_analysis = now - def get_prices_tomorrow(self): - return self.get_timestamped_prices(self.get_data_tomorrow()) + # ANALYSIS: filter the hourprices on which to apply the analysis + def _filter_analysis_window(self, data): + last_hour = dt.now().replace(minute=0, second=0, microsecond=0) - def get_prices(self): - if len(self.data) > 48: - return self.get_timestamped_prices( - {hour: price for hour, price in self.data.items() if hour >= self.today} + if self.analysis_window == ANALYSIS_WINDOW["today"]: + start = self.today + end = start + timedelta(days=1) + self.logger.debug( + f"Filter dataset for prices today {start} - {end} -> refresh each day" ) - return self.get_timestamped_prices( - { - hour: price - for hour, price in self.data.items() - if hour >= self.today - timedelta(days=1) - } - ) - def get_data(self, date): - return {k: v for k, v in self.data.items() if k.date() == date.date()} + elif self.analysis_window == ANALYSIS_WINDOW["sliding-24"]: + start = last_hour - timedelta(hours=12) + end = start + timedelta(hours=24) + self.logger.debug( + f"Filter dataset to surrounding 24hrs {start} - {end} -> refresh each hour" + ) - def get_data_today(self): - return {k: v for k, v in self.data.items() if k.date() == self.today.date()} + elif self.analysis_window == ANALYSIS_WINDOW["sliding-12"]: + start = last_hour - timedelta(hours=6) + end = start + timedelta(hours=12) + self.logger.debug( + f"Filter dataset to surrounding 12hrs {start} - {end} -> refresh each hour" + ) - def get_data_tomorrow(self): - return { - k: v - for k, v in self.data.items() - if k.date() == self.today.date() + timedelta(days=1) - } + elif self.analysis_window == ANALYSIS_WINDOW["forward-24"]: + start = last_hour + end = start + timedelta(hours=24) + self.logger.debug( + f"Filter dataset to upcomming 24hrs {start} - {end} -> refresh each hour" + ) - def get_next_hourprice(self) -> int: - return self.data[ - dt.now().replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) - ] + elif self.analysis_window == ANALYSIS_WINDOW["forward-12"]: + start = last_hour + end = start + timedelta(hours=12) + self.logger.debug( + f"Filter dataset to upcomming 12hrs {start} - {end} -> refresh each hour" + ) - def get_current_hourprice(self) -> int: - return self.data[dt.now().replace(minute=0, second=0, microsecond=0)] + else: # self.analysis_window == ANALYSIS_WINDOW["publish"]: + self.logger.debug( + f"Window is set to {self.analysis_window} and therefore we use the 48hrs dataset" + ) + return self.get_48hrs_data() - def get_avg_price(self): - return round( - sum(self.filtered_hourprices.values()) - / len(self.filtered_hourprices.values()), - 5, - ) + return {hour: price for hour, price in data.items() if start < hour < end} + # ANALYSIS: Get max price in analysis window def get_max_price(self): return max(self.filtered_hourprices.values()) + # ANALYSIS: Get min price in analysis window def get_min_price(self): return min(self.filtered_hourprices.values()) + # ANALYSIS: Get timestamp of max price in analysis window def get_max_time(self): return max(self.filtered_hourprices, key=self.filtered_hourprices.get) + # ANALYSIS: Get timestamp of min price in analysis window def get_min_time(self): return min(self.filtered_hourprices, key=self.filtered_hourprices.get) + # ANALYSIS: Get avg price in analysis window + # Tip: import mean() from statistics + def get_avg_price(self): + prices = self.filtered_hourprices.values() + return round(sum(prices) / len(prices), 5) + + # ANALYSIS: Get percentage of current price relative to maximum in analysis window def get_percentage_of_max(self): return round(self.get_current_hourprice() / self.get_max_price() * 100, 1) + # ANALYSIS: Get percentage of current price relative to spread (max-min) of analysis window def get_percentage_of_range(self): min = self.get_min_price() spread = self.get_max_price() - min current = self.get_current_hourprice() - min return round(current / spread * 100, 1) - def get_timestamped_prices(self, hourprices): - list = [] - for hour, price in hourprices.items(): - str_hour = str(hour) - list.append({"time": str_hour, "price": price}) - return list + # -------------------------------------------------------------------------------------------------------------------------------- + # SERVICES: returns data from the coordinator cache, or directly from ENTSO when not availble + # danger here for processing requests with huge periods -> suggest to limit to the 72 hrs of cached data + async def get_energy_prices(self, start_date, end_date): + # check if we have the data already + if ( + len(self.get_data(start_date)) > MIN_HOURS + and len(self.get_data(end_date)) > MIN_HOURS + ): + self.logger.debug("return prices from coordinator cache.") + return { + k: v + for k, v in self.data.items() + if k.date() >= start_date.date() and k.date() <= end_date.date() + } + return self.parse_hourprices(await self.fetch_prices(start_date, end_date)) diff --git a/custom_components/entsoe/sensor.py b/custom_components/entsoe/sensor.py index 4538f67..1347027 100644 --- a/custom_components/entsoe/sensor.py +++ b/custom_components/entsoe/sensor.py @@ -224,13 +224,10 @@ async def async_update(self) -> None: utcnow().replace(minute=0, second=0) + timedelta(hours=1), ) - # ensure the calculated data is refreshed by the changing hour - self.coordinator.sync_calculator() + # ensure the analysis is refreshed by the changing hour + self.coordinator.refresh_analysis() - if ( - self.coordinator.data is not None - and self.coordinator.today_data_available() - ): + if self.coordinator.data is not None: value: Any = None try: # _LOGGER.debug(f"current coordinator.data value: {self.coordinator.data}")