diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index f5110f41e59..3e05fe4bd90 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -27,7 +27,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import ( @@ -120,16 +120,10 @@ def _validate_supported_features( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Todo entities.""" - component = hass.data[DATA_COMPONENT] = EntityComponent[TodoListEntity]( + component = hass.data[DATA_COMPONENT] = TodoEntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) - frontend.async_register_built_in_panel(hass, "todo", "todo", "mdi:clipboard-list") - - websocket_api.async_register_command(hass, websocket_handle_subscribe_todo_items) - websocket_api.async_register_command(hass, websocket_handle_todo_item_list) - websocket_api.async_register_command(hass, websocket_handle_todo_item_move) - component.async_register_entity_service( TodoServices.ADD_ITEM, vol.All( @@ -318,6 +312,52 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self.async_update_listeners() +class TodoEntityComponent(EntityComponent[TodoListEntity]): + """To-do entity component. + + Sets up frontend resources and websocket API when the first platform is added. + """ + + _frontend_loaded: bool = False + + async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: + """Set up a config entry.""" + result = await super().async_setup_entry(config_entry) + + if not self._frontend_loaded: + self._register_frontend_resources() + + return result + + async def async_setup_platform( + self, + platform_type: str, + platform_config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up a platform for this component.""" + await super().async_setup_platform( + platform_type, platform_config, discovery_info + ) + + if not self._frontend_loaded: + self._register_frontend_resources() + + def _register_frontend_resources(self) -> None: + """Register frontend resources for to-do.""" + self._frontend_loaded = True + + frontend.async_register_built_in_panel( + self.hass, "todo", "todo", "mdi:clipboard-list" + ) + + websocket_api.async_register_command( + self.hass, websocket_handle_subscribe_todo_items + ) + websocket_api.async_register_command(self.hass, websocket_handle_todo_item_list) + websocket_api.async_register_command(self.hass, websocket_handle_todo_item_move) + + @websocket_api.websocket_command( { vol.Required("type"): "todo/item/subscribe", diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index e7a9fd364f9..363ffc119f7 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -2,6 +2,7 @@ import datetime from typing import Any +from unittest.mock import AsyncMock import zoneinfo import pytest @@ -22,7 +23,7 @@ from homeassistant.components.todo import ( TodoServices, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( HomeAssistantError, @@ -33,6 +34,7 @@ from homeassistant.setup import async_setup_component from . import create_mock_platform +from tests.common import MockPlatform, mock_platform from tests.typing import WebSocketGenerator ITEM_1 = { @@ -1299,3 +1301,47 @@ async def test_async_subscribe_updates( unsub() test_entity.async_write_ha_state() assert len(received_updates) == 4 + + +async def test_frontend_resources_registered_after_first_config_entry_setup( + hass: HomeAssistant, +) -> None: + """Test that frontend resources are registered after the first config entry is set up.""" + await async_setup_component(hass, "http", {}) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + await create_mock_platform(hass, []) + assert set(hass.data["frontend_panels"]) == {"todo"} + assert set(hass.data["websocket_api"]) == { + "todo/item/list", + "todo/item/move", + "todo/item/subscribe", + } + + +async def test_frontend_resources_registered_after_first_platform_setup( + hass: HomeAssistant, +) -> None: + """Test that frontend resources are registered after the first platform is set up.""" + mock_platform( + hass, + f"test.{DOMAIN}", + MockPlatform(async_setup_platform=AsyncMock()), + ) + + await async_setup_component(hass, "http", {}) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + assert set(hass.data["frontend_panels"]) == {"todo"} + assert set(hass.data["websocket_api"]) == { + "todo/item/list", + "todo/item/move", + "todo/item/subscribe", + }