diff --git a/supervisor/homeassistant/core.py b/supervisor/homeassistant/core.py index bdbe6217e..e89c6e44e 100644 --- a/supervisor/homeassistant/core.py +++ b/supervisor/homeassistant/core.py @@ -77,7 +77,7 @@ class HomeAssistantCore(JobGroup): super().__init__(coresys, JOB_GROUP_HOME_ASSISTANT_CORE) self.instance: DockerHomeAssistant = DockerHomeAssistant(coresys) self._error_state: bool = False - self._core_config: dict[str, Any] | None = None + self._cached_core_config: dict[str, Any] | None = None self._watchdog_listener: EventListener | None = None @property @@ -86,9 +86,13 @@ class HomeAssistantCore(JobGroup): return self._error_state @property - def core_config(self) -> dict[str, Any] | None: - """Return cached Core config or None if not available.""" - return self._core_config + def cached_core_config(self) -> dict[str, Any] | None: + """Return Core config captured when Core reached RUNNING state. + + This is a snapshot from startup and does not reflect integrations + loaded at runtime (e.g. a user installing an integration after boot). + """ + return self._cached_core_config async def ensure_started(self) -> None: """Ensure Home Assistant Core is running and setup is complete. @@ -347,7 +351,7 @@ class HomeAssistantCore(JobGroup): await _update(to_version) if not self.error_state and rollback: - config = self._core_config + config = self.cached_core_config # Verify that the frontend is loaded if config is None: @@ -432,7 +436,7 @@ class HomeAssistantCore(JobGroup): ) async def stop(self, *, remove_container: bool = False) -> None: """Stop Home Assistant Docker.""" - self._core_config = None + self._cached_core_config = None try: return await self.instance.stop(remove_container=remove_container) except DockerError as err: @@ -543,7 +547,7 @@ class HomeAssistantCore(JobGroup): return _LOGGER.info("Wait until Home Assistant is ready") - self._core_config = None + self._cached_core_config = None deadline = datetime.now() + STARTUP_API_RESPONSE_TIMEOUT last_state = None @@ -570,7 +574,7 @@ class HomeAssistantCore(JobGroup): _LOGGER.info("Detect a running Home Assistant instance") self._error_state = False with suppress(HomeAssistantAPIError): - self._core_config = ( + self._cached_core_config = ( await self.sys_homeassistant.api.get_config() ) return diff --git a/supervisor/homeassistant/module.py b/supervisor/homeassistant/module.py index 0f0592d44..2f69a4c20 100644 --- a/supervisor/homeassistant/module.py +++ b/supervisor/homeassistant/module.py @@ -358,7 +358,7 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes): ): return - if not (config := self.core.core_config): + if not (config := self.core.cached_core_config): _LOGGER.debug("Core config not available, skipping hardware event") return diff --git a/tests/api/test_homeassistant.py b/tests/api/test_homeassistant.py index 47cdd2321..9c00392d1 100644 --- a/tests/api/test_homeassistant.py +++ b/tests/api/test_homeassistant.py @@ -279,7 +279,7 @@ async def test_api_progress_updates_home_assistant_update( logs = load_json_fixture("docker_pull_image_log.json") coresys.docker.images.pull.return_value = AsyncIterator(logs) coresys.homeassistant.version = AwesomeVersion("2025.8.0") - coresys.homeassistant.core._core_config = {"components": ["frontend"]} + coresys.homeassistant.core._cached_core_config = {"components": ["frontend"]} # noqa: SLF001 # pylint: disable=protected-access with ( patch.object( @@ -445,7 +445,7 @@ async def test_update_frontend_check_success(api_client: TestClient, coresys: Co """Test that update succeeds when frontend check passes.""" coresys.hardware.disk.get_disk_free_space = lambda x: 5000 coresys.homeassistant.version = AwesomeVersion("2025.8.0") - coresys.homeassistant.core._core_config = {"components": ["frontend"]} + coresys.homeassistant.core._cached_core_config = {"components": ["frontend"]} # noqa: SLF001 # pylint: disable=protected-access with ( patch.object( @@ -483,7 +483,7 @@ async def test_update_frontend_check_fails_triggers_rollback( # Rollback succeeds coresys.homeassistant.version = AwesomeVersion("2025.8.0") - coresys.homeassistant.core._core_config = {"components": ["frontend"]} + coresys.homeassistant.core._cached_core_config = {"components": ["frontend"]} # noqa: SLF001 # pylint: disable=protected-access with ( patch.object(DockerInterface, "update", new=mock_update), diff --git a/tests/homeassistant/test_api.py b/tests/homeassistant/test_api.py index 35125ced3..d8807c9f0 100644 --- a/tests/homeassistant/test_api.py +++ b/tests/homeassistant/test_api.py @@ -9,6 +9,15 @@ import pytest from supervisor.coresys import CoreSys from supervisor.exceptions import HomeAssistantAPIError +from supervisor.homeassistant.api import HomeAssistantAPI + + +@pytest.fixture(autouse=True) +def _restore_get_config(coresys: CoreSys) -> None: + """Restore real get_config replaced by global mock in conftest.""" + coresys.homeassistant.api.get_config = HomeAssistantAPI.get_config.__get__( + coresys.homeassistant.api + ) async def test_check_frontend_available_success(coresys: CoreSys): diff --git a/tests/homeassistant/test_core.py b/tests/homeassistant/test_core.py index 29e895398..bacd627ca 100644 --- a/tests/homeassistant/test_core.py +++ b/tests/homeassistant/test_core.py @@ -472,7 +472,7 @@ async def test_api_check_success( assert coresys.homeassistant.api.get_api_state.call_count == 1 assert "Detect a running Home Assistant instance" in caplog.text - assert coresys.homeassistant.core.core_config == {"components": ["frontend"]} + assert coresys.homeassistant.core.cached_core_config == {"components": ["frontend"]} async def test_api_check_database_migration( @@ -511,7 +511,7 @@ async def test_api_check_database_migration( assert coresys.homeassistant.api.get_api_state.call_count == 51 assert "Detect a running Home Assistant instance" in caplog.text - assert coresys.homeassistant.core.core_config == {"components": ["frontend"]} + assert coresys.homeassistant.core.cached_core_config == {"components": ["frontend"]} async def test_api_check_timeout_clears_core_config( @@ -524,7 +524,7 @@ async def test_api_check_timeout_clears_core_config( coresys.homeassistant.api.get_api_state.return_value = None # Seed the cache so we can verify it gets cleared - coresys.homeassistant.core._core_config = {"components": ["frontend"]} + coresys.homeassistant.core._cached_core_config = {"components": ["frontend"]} # noqa: SLF001 # pylint: disable=protected-access async def mock_instance_start(*_): container.show.return_value["State"]["Status"] = "running" @@ -545,7 +545,7 @@ async def test_api_check_timeout_clears_core_config( ): await coresys.homeassistant.core.start() - assert coresys.homeassistant.core.core_config is None + assert coresys.homeassistant.core.cached_core_config is None async def test_stop_clears_core_config(coresys: CoreSys, container: DockerContainer): @@ -554,12 +554,12 @@ async def test_stop_clears_core_config(coresys: CoreSys, container: DockerContai container.show.return_value["State"]["Running"] = True # Seed the cache - coresys.homeassistant.core._core_config = {"components": ["frontend", "usb"]} - assert coresys.homeassistant.core.core_config is not None + coresys.homeassistant.core._cached_core_config = {"components": ["frontend", "usb"]} # noqa: SLF001 # pylint: disable=protected-access + assert coresys.homeassistant.core.cached_core_config is not None await coresys.homeassistant.core.stop() - assert coresys.homeassistant.core.core_config is None + assert coresys.homeassistant.core.cached_core_config is None async def test_core_loads_wrong_image_for_machine(