diff --git a/.strict-typing b/.strict-typing index daa4a56dead..daf2baabf67 100644 --- a/.strict-typing +++ b/.strict-typing @@ -152,6 +152,7 @@ homeassistant.components.hardkernel.* homeassistant.components.hardware.* homeassistant.components.here_travel_time.* homeassistant.components.history.* +homeassistant.components.holiday.* homeassistant.components.homeassistant.exposed_entities homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant_alerts.* diff --git a/CODEOWNERS b/CODEOWNERS index 33587dec721..e618db415c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -522,6 +522,8 @@ build.json @home-assistant/supervisor /tests/components/hive/ @Rendili @KJonline /homeassistant/components/hlk_sw16/ @jameshilliard /tests/components/hlk_sw16/ @jameshilliard +/homeassistant/components/holiday/ @jrieger +/tests/components/holiday/ @jrieger /homeassistant/components/home_connect/ @DavidMStraub /tests/components/home_connect/ @DavidMStraub /homeassistant/components/home_plus_control/ @chemaaa diff --git a/homeassistant/components/holiday/__init__.py b/homeassistant/components/holiday/__init__.py new file mode 100644 index 00000000000..224b1b01294 --- /dev/null +++ b/homeassistant/components/holiday/__init__.py @@ -0,0 +1,20 @@ +"""The Holiday integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS: list[Platform] = [Platform.CALENDAR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Holiday from a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py new file mode 100644 index 00000000000..bb9a332cb73 --- /dev/null +++ b/homeassistant/components/holiday/calendar.py @@ -0,0 +1,134 @@ +"""Holiday Calendar.""" +from __future__ import annotations + +from datetime import datetime + +from holidays import HolidayBase, country_holidays + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from .const import CONF_PROVINCE, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Holiday Calendar config entry.""" + country: str = config_entry.data[CONF_COUNTRY] + province: str | None = config_entry.data.get(CONF_PROVINCE) + language = hass.config.language + + obj_holidays = country_holidays( + country, + subdiv=province, + years={dt_util.now().year, dt_util.now().year + 1}, + language=language, + ) + if language == "en": + for lang in obj_holidays.supported_languages: + if lang.startswith("en"): + obj_holidays = country_holidays( + country, + subdiv=province, + years={dt_util.now().year, dt_util.now().year + 1}, + language=lang, + ) + language = lang + break + + async_add_entities( + [ + HolidayCalendarEntity( + config_entry.title, + country, + province, + language, + obj_holidays, + config_entry.entry_id, + ) + ], + True, + ) + + +class HolidayCalendarEntity(CalendarEntity): + """Representation of a Holiday Calendar element.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + name: str, + country: str, + province: str | None, + language: str, + obj_holidays: HolidayBase, + unique_id: str, + ) -> None: + """Initialize HolidayCalendarEntity.""" + self._country = country + self._province = province + self._location = name + self._language = language + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + entry_type=DeviceEntryType.SERVICE, + name=name, + ) + self._obj_holidays = obj_holidays + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + next_holiday = None + for holiday_date, holiday_name in sorted( + self._obj_holidays.items(), key=lambda x: x[0] + ): + if holiday_date >= dt_util.now().date(): + next_holiday = (holiday_date, holiday_name) + break + + if next_holiday is None: + return None + + return CalendarEvent( + summary=next_holiday[1], + start=next_holiday[0], + end=next_holiday[0], + location=self._location, + ) + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + obj_holidays = country_holidays( + self._country, + subdiv=self._province, + years=list({start_date.year, end_date.year}), + language=self._language, + ) + + event_list: list[CalendarEvent] = [] + + for holiday_date, holiday_name in obj_holidays.items(): + if start_date.date() <= holiday_date <= end_date.date(): + event = CalendarEvent( + summary=holiday_name, + start=holiday_date, + end=holiday_date, + location=self._location, + ) + event_list.append(event) + + return event_list diff --git a/homeassistant/components/holiday/config_flow.py b/homeassistant/components/holiday/config_flow.py new file mode 100644 index 00000000000..93ff2772eb8 --- /dev/null +++ b/homeassistant/components/holiday/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow for Holiday integration.""" +from __future__ import annotations + +from typing import Any + +from babel import Locale +from holidays import list_supported_countries +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_COUNTRY +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import ( + CountrySelector, + CountrySelectorConfig, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import CONF_PROVINCE, DOMAIN + +SUPPORTED_COUNTRIES = list_supported_countries(include_aliases=False) + + +class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Holiday.""" + + VERSION = 1 + + data: dict[str, Any] = {} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is not None: + self.data = user_input + + selected_country = self.data[CONF_COUNTRY] + + if SUPPORTED_COUNTRIES[selected_country]: + return await self.async_step_province() + + self._async_abort_entries_match({CONF_COUNTRY: user_input[CONF_COUNTRY]}) + + locale = Locale(self.hass.config.language) + title = locale.territories[selected_country] + return self.async_create_entry(title=title, data=self.data) + + user_schema = vol.Schema( + { + vol.Optional( + CONF_COUNTRY, default=self.hass.config.country + ): CountrySelector( + CountrySelectorConfig( + countries=list(SUPPORTED_COUNTRIES), + ) + ), + } + ) + + return self.async_show_form(step_id="user", data_schema=user_schema) + + async def async_step_province( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the province step.""" + if user_input is not None: + combined_input: dict[str, Any] = {**self.data, **user_input} + + country = combined_input[CONF_COUNTRY] + province = combined_input.get(CONF_PROVINCE) + + self._async_abort_entries_match( + { + CONF_COUNTRY: country, + CONF_PROVINCE: province, + } + ) + + locale = Locale(self.hass.config.language) + province_str = f", {province}" if province else "" + name = f"{locale.territories[country]}{province_str}" + + return self.async_create_entry(title=name, data=combined_input) + + province_schema = vol.Schema( + { + vol.Optional(CONF_PROVINCE): SelectSelector( + SelectSelectorConfig( + options=SUPPORTED_COUNTRIES[self.data[CONF_COUNTRY]], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + return self.async_show_form(step_id="province", data_schema=province_schema) diff --git a/homeassistant/components/holiday/const.py b/homeassistant/components/holiday/const.py new file mode 100644 index 00000000000..5d2a567a488 --- /dev/null +++ b/homeassistant/components/holiday/const.py @@ -0,0 +1,6 @@ +"""Constants for the Holiday integration.""" +from typing import Final + +DOMAIN: Final = "holiday" + +CONF_PROVINCE: Final = "province" diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json new file mode 100644 index 00000000000..f73577bddee --- /dev/null +++ b/homeassistant/components/holiday/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "holiday", + "name": "Holiday", + "codeowners": ["@jrieger"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/holiday", + "iot_class": "local_polling", + "requirements": ["holidays==0.37", "babel==2.13.1"] +} diff --git a/homeassistant/components/holiday/strings.json b/homeassistant/components/holiday/strings.json new file mode 100644 index 00000000000..4762a48c659 --- /dev/null +++ b/homeassistant/components/holiday/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Already configured. Only a single configuration for country/province combination possible." + }, + "step": { + "user": { + "data": { + "country": "Country" + } + }, + "province": { + "data": { + "province": "Province" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e83a2a74405..aa13faaf501 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -201,6 +201,7 @@ FLOWS = { "hisense_aehw4a1", "hive", "hlk_sw16", + "holiday", "home_connect", "home_plus_control", "homeassistant_sky_connect", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 56b0fa4ef9d..636d1030850 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2408,6 +2408,12 @@ "config_flow": true, "iot_class": "local_push" }, + "holiday": { + "name": "Holiday", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "home_connect": { "name": "Home Connect", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 05525d03300..83bc95a940b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1281,6 +1281,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.holiday.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homeassistant.exposed_entities] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 5f811f514a5..c9b8f66647f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -504,6 +504,9 @@ azure-eventhub==5.11.1 # homeassistant.components.azure_service_bus azure-servicebus==7.10.0 +# homeassistant.components.holiday +babel==2.13.1 + # homeassistant.components.baidu baidu-aip==1.6.6 @@ -1013,6 +1016,7 @@ hlk-sw16==0.0.9 # homeassistant.components.pi_hole hole==0.8.0 +# homeassistant.components.holiday # homeassistant.components.workday holidays==0.37 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97a41ba9fe4..9ba43ff0dbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -438,6 +438,9 @@ axis==48 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 +# homeassistant.components.holiday +babel==2.13.1 + # homeassistant.components.homekit base36==0.1.1 @@ -800,6 +803,7 @@ hlk-sw16==0.0.9 # homeassistant.components.pi_hole hole==0.8.0 +# homeassistant.components.holiday # homeassistant.components.workday holidays==0.37 diff --git a/tests/components/holiday/__init__.py b/tests/components/holiday/__init__.py new file mode 100644 index 00000000000..e906586aabc --- /dev/null +++ b/tests/components/holiday/__init__.py @@ -0,0 +1 @@ +"""Tests for the Holiday integration.""" diff --git a/tests/components/holiday/conftest.py b/tests/components/holiday/conftest.py new file mode 100644 index 00000000000..d9b0d1a5788 --- /dev/null +++ b/tests/components/holiday/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the Holiday tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.holiday.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py new file mode 100644 index 00000000000..06011fb8e6b --- /dev/null +++ b/tests/components/holiday/test_calendar.py @@ -0,0 +1,229 @@ +"""Tests for calendar platform of Holiday integration.""" +from datetime import datetime, timedelta + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.calendar import ( + DOMAIN as CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, +) +from homeassistant.components.holiday.const import CONF_PROVINCE, DOMAIN +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_holiday_calendar_entity( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test HolidayCalendarEntity functionality.""" + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_COUNTRY: "US", CONF_PROVINCE: "AK"}, + title="United States, AK", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + await async_setup_component(hass, "calendar", {}) + await hass.async_block_till_done() + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.united_states_ak", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.united_states_ak": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "New Year's Day", + "location": "United States, AK", + } + ] + } + } + + state = hass.states.get("calendar.united_states_ak") + assert state is not None + assert state.state == "on" + + # Test holidays for the next year + freezer.move_to(datetime(2023, 12, 31, 12, tzinfo=dt_util.UTC)) + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.united_states_ak", + "end_date_time": dt_util.now() + timedelta(days=1), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.united_states_ak": { + "events": [ + { + "start": "2024-01-01", + "end": "2024-01-02", + "summary": "New Year's Day", + "location": "United States, AK", + } + ] + } + } + + +async def test_default_language( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test default language.""" + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_COUNTRY: "FR", CONF_PROVINCE: "BL"}, + title="France, BL", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Test French calendar with English language + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.france_bl", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.france_bl": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "New Year's Day", + "location": "France, BL", + } + ] + } + } + + # Test French calendar with French language + hass.config.language = "fr" + + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.france_bl", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.france_bl": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "Jour de l'an", + "location": "France, BL", + } + ] + } + } + + +async def test_no_language( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test language defaults to English if language not exist.""" + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_COUNTRY: "AL"}, + title="Albania", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.albania", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.albania": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "New Year's Day", + "location": "Albania", + } + ] + } + } + + +async def test_no_next_event( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if there is no next event.""" + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_COUNTRY: "DE"}, + title="Germany", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Move time to out of reach + freezer.move_to(datetime(dt_util.now().year + 5, 1, 1, 12, tzinfo=dt_util.UTC)) + async_fire_time_changed(hass) + + state = hass.states.get("calendar.germany") + assert state is not None + assert state.state == "off" + assert state.attributes == {"friendly_name": "Germany"} diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py new file mode 100644 index 00000000000..e99d310762e --- /dev/null +++ b/tests/components/holiday/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Holiday config flow.""" +from unittest.mock import AsyncMock + +import pytest + +from homeassistant import config_entries +from homeassistant.components.holiday.const import CONF_PROVINCE, DOMAIN +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "DE", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROVINCE: "BW", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Germany, BW" + assert result3["data"] == { + "country": "DE", + "province": "BW", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_no_subdivision(hass: HomeAssistant) -> None: + """Test we get the forms correctly without subdivision.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "SE", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Sweden" + assert result2["data"] == { + "country": "SE", + } + + +async def test_form_translated_title(hass: HomeAssistant) -> None: + """Test the title gets translated.""" + hass.config.language = "de" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "SE", + }, + ) + await hass.async_block_till_done() + + assert result2["title"] == "Schweden" + + +async def test_single_combination_country_province(hass: HomeAssistant) -> None: + """Test that configuring more than one instance is rejected.""" + data_de = { + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BW", + } + data_se = { + CONF_COUNTRY: "SE", + } + MockConfigEntry(domain=DOMAIN, data=data_de).add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, data=data_se).add_to_hass(hass) + + # Test for country without subdivisions + result_se = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=data_se, + ) + assert result_se["type"] == FlowResultType.ABORT + assert result_se["reason"] == "already_configured" + + # Test for country with subdivisions + result_de_step1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=data_de, + ) + assert result_de_step1["type"] == FlowResultType.FORM + + result_de_step2 = await hass.config_entries.flow.async_configure( + result_de_step1["flow_id"], + { + CONF_PROVINCE: data_de[CONF_PROVINCE], + }, + ) + assert result_de_step2["type"] == FlowResultType.ABORT + assert result_de_step2["reason"] == "already_configured" diff --git a/tests/components/holiday/test_init.py b/tests/components/holiday/test_init.py new file mode 100644 index 00000000000..a044e390a68 --- /dev/null +++ b/tests/components/holiday/test_init.py @@ -0,0 +1,29 @@ +"""Tests for the Holiday integration.""" + +from homeassistant.components.holiday.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_CONFIG_DATA = { + "country": "Germany", + "province": "BW", +} + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + state: ConfigEntryState = entry.state + assert state == ConfigEntryState.NOT_LOADED