1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Set up calendar frontend resources when first platform is set up

This commit is contained in:
Erik
2026-03-30 14:53:13 +02:00
parent 51a5f5793f
commit cfc353be69
5 changed files with 194 additions and 19 deletions

View File

@@ -37,7 +37,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
@@ -308,19 +308,10 @@ SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for calendars."""
component = hass.data[DATA_COMPONENT] = EntityComponent[CalendarEntity](
component = hass.data[DATA_COMPONENT] = CalendarEntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
frontend.async_register_built_in_panel(hass, "calendar", "calendar", "mdi:calendar")
websocket_api.async_register_command(hass, handle_calendar_event_create)
websocket_api.async_register_command(hass, handle_calendar_event_delete)
websocket_api.async_register_command(hass, handle_calendar_event_update)
component.async_register_entity_service(
CREATE_EVENT_SERVICE,
CREATE_EVENT_SCHEMA,
@@ -667,6 +658,57 @@ class CalendarEntity(Entity):
raise NotImplementedError
class CalendarEntityComponent(EntityComponent[CalendarEntity]):
"""Calendar entity component.
Sets up frontend resources and websocket API when the first platform is added.
"""
async def async_setup_entry(self, config_entry: ConfigEntry) -> bool:
"""Set up a config entry."""
num_platforms_before = len(self._platforms)
result = await super().async_setup_entry(config_entry)
# There will always be at least one platform, the calendar platform itself.
if num_platforms_before == 1 and len(self._platforms) == 2:
# First calendar platform was added, set up frontend resources
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."""
num_platforms_before = len(self._platforms)
await super().async_setup_platform(
platform_type, platform_config, discovery_info
)
# There will always be at least one platform, the calendar platform itself.
if num_platforms_before == 1 and len(self._platforms) == 2:
# First calendar platform was added, set up frontend resources
self._register_frontend_resources()
def _register_frontend_resources(self) -> None:
"""Register frontend resources for calendar."""
self.hass.http.register_view(CalendarListView(self))
self.hass.http.register_view(CalendarEventView(self))
frontend.async_register_built_in_panel(
self.hass, "calendar", "calendar", "mdi:calendar"
)
websocket_api.async_register_command(self.hass, handle_calendar_event_create)
websocket_api.async_register_command(self.hass, handle_calendar_event_delete)
websocket_api.async_register_command(self.hass, handle_calendar_event_update)
class CalendarEventView(http.HomeAssistantView):
"""View to retrieve calendar content."""

View File

@@ -17,7 +17,11 @@ from homeassistant.components.calendar import (
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from tests.common import (
@@ -125,12 +129,12 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
@pytest.fixture
def mock_setup_integration(
def mock_setup_config_entry_integration(
hass: HomeAssistant,
config_flow_fixture: None,
test_entities: list[CalendarEntity],
) -> None:
"""Fixture to set up a mock integration."""
"""Fixture to set up a mock integration with config entry."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
@@ -178,6 +182,29 @@ def mock_setup_integration(
)
@pytest.fixture
def mock_setup_platform_integration(
hass: HomeAssistant,
config_flow_fixture: None,
test_entities: list[CalendarEntity],
) -> None:
"""Fixture to set up a mock integration without config entry."""
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
pass
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_platform=async_setup_platform),
)
@pytest.fixture(name="test_entities")
def mock_test_entities() -> list[MockCalendarEntity]:
"""Fixture that holdes the fake entities created during the test."""

View File

@@ -8,6 +8,7 @@ from http import HTTPStatus
import re
from typing import Any
from aiohttp.test_utils import TestClient
from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -20,10 +21,12 @@ from homeassistant.components.calendar import (
CalendarEntity,
CalendarEntityDescription,
)
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .conftest import MockCalendarEntity, MockConfigEntry
@@ -50,12 +53,23 @@ def mock_set_frozen_time(frozen_time: str | None) -> Generator[None]:
yield
@pytest.fixture(name="setup_platform", autouse=True)
async def mock_setup_platform(
@pytest.fixture(name="setup_calendar_integration")
async def mock_setup_calendar_integration(
hass: HomeAssistant,
set_time_zone: None,
frozen_time: str | None,
mock_setup_integration: None,
) -> None:
"""Fixture to setup the calendar integration."""
await async_setup_component(hass, DOMAIN, {})
@pytest.fixture(name="setup_calendar_config_entry_platform")
# @pytest.fixture(name="setup_platform", autouse=True)
async def mock_setup_config_entry_platform(
hass: HomeAssistant,
set_time_zone: None,
frozen_time: str | None,
mock_setup_config_entry_integration: None,
config_entry: MockConfigEntry,
) -> None:
"""Fixture to setup platforms used in the test and fixtures are set up in the right order."""
@@ -63,6 +77,7 @@ async def mock_setup_platform(
await hass.async_block_till_done()
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_events_http_api(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
@@ -78,6 +93,7 @@ async def test_events_http_api(
assert events[0]["summary"] == "Future Event"
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_events_http_api_missing_fields(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
@@ -87,6 +103,7 @@ async def test_events_http_api_missing_fields(
assert response.status == HTTPStatus.BAD_REQUEST
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_events_http_api_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
@@ -106,6 +123,7 @@ async def test_events_http_api_error(
assert await response.json() == {"message": "Error reading events: Failure"}
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_events_http_api_dates_wrong_order(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
@@ -119,6 +137,7 @@ async def test_events_http_api_dates_wrong_order(
assert response.status == HTTPStatus.BAD_REQUEST
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_calendars_http_api(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
@@ -134,6 +153,7 @@ async def test_calendars_http_api(
]
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
@pytest.mark.parametrize(
("payload", "code"),
[
@@ -222,6 +242,7 @@ async def test_unsupported_websocket(
assert resp["error"].get("code") == code
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_unsupported_create_event_service(hass: HomeAssistant) -> None:
"""Test unsupported service call."""
with pytest.raises(
@@ -242,6 +263,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None:
)
@pytest.mark.usefixtures("setup_calendar_integration")
@pytest.mark.parametrize(
("date_fields", "expected_error", "error_match"),
[
@@ -417,6 +439,7 @@ async def test_create_event_service_invalid_params(
)
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
@pytest.mark.parametrize(
"frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"]
)
@@ -477,6 +500,7 @@ async def test_list_events_service(
assert response == expected
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
@pytest.mark.parametrize(
("service"),
[
@@ -516,6 +540,7 @@ async def test_list_events_service_duration(
assert response == snapshot
@pytest.mark.usefixtures("setup_calendar_integration")
async def test_list_events_positive_duration(hass: HomeAssistant) -> None:
"""Test listing events requires a positive duration."""
with pytest.raises(vol.Invalid, match="should be positive"):
@@ -531,6 +556,7 @@ async def test_list_events_positive_duration(hass: HomeAssistant) -> None:
)
@pytest.mark.usefixtures("setup_calendar_integration")
async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None:
"""Test listing events specifying fields that are exclusive."""
end = dt_util.now() + timedelta(days=1)
@@ -549,6 +575,7 @@ async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None:
)
@pytest.mark.usefixtures("setup_calendar_integration")
async def test_list_events_missing_fields(hass: HomeAssistant) -> None:
"""Test listing events missing some required fields."""
with pytest.raises(vol.Invalid, match="at least one of"):
@@ -563,6 +590,7 @@ async def test_list_events_missing_fields(hass: HomeAssistant) -> None:
)
@pytest.mark.usefixtures("setup_calendar_integration")
@pytest.mark.parametrize(
"frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"]
)
@@ -613,6 +641,7 @@ async def test_list_events_service_same_dates(
)
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_calendar_initial_color_valid(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@@ -628,6 +657,7 @@ async def test_calendar_initial_color_valid(
assert entry.options.get(DOMAIN, {}).get("color") == "#FF0000"
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
@pytest.mark.parametrize(
"invalid_initial_color",
[
@@ -654,6 +684,7 @@ async def test_calendar_initial_color_invalid(
assert entity.get_initial_entity_options() is None
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
async def test_calendar_initial_color_none(
hass: HomeAssistant,
test_entities: list[MockCalendarEntity],
@@ -664,6 +695,7 @@ async def test_calendar_initial_color_none(
assert entity.get_initial_entity_options() is None
@pytest.mark.usefixtures("setup_calendar_config_entry_platform")
@pytest.mark.parametrize(
("description_color", "attr_color", "expected_color"),
[
@@ -715,3 +747,77 @@ async def test_calendar_initial_color_precedence(
entity = TestCalendarEntity(description_color, attr_color)
assert entity.initial_color == expected_color
async def test_services_registered_after_integration_setup(hass: HomeAssistant) -> None:
"""Test that services are registered after integration setup."""
assert DOMAIN not in hass.services.async_services()
await async_setup_component(hass, DOMAIN, {})
assert set(hass.services.async_services()[DOMAIN]) == {"create_event", "get_events"}
async def _assert_http_api_responses(
client: TestClient,
expected_status_calendar_list: HTTPStatus,
expected_status_calendar_event: HTTPStatus,
) -> None:
"""Assert that the HTTP API endpoints return the expected status."""
response = await client.get("/api/calendars")
assert response.status == expected_status_calendar_list
response = await client.get("/api/calendars/calendar.calendar_1")
assert response.status == expected_status_calendar_event
async def test_frontend_resources_registered_after_first_config_entry_setup(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
set_time_zone: None,
frozen_time: str | None,
mock_setup_config_entry_integration: None,
config_entry: MockConfigEntry,
) -> None:
"""Test that frontend resources are registered after the first config entry is set up."""
await async_setup_component(hass, "http", {})
client = await hass_client()
await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND)
assert "frontend_panels" not in hass.data
assert "websocket_api" not in hass.data
await async_setup_component(hass, DOMAIN, {})
await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND)
assert "frontend_panels" not in hass.data
assert "websocket_api" not in hass.data
assert await hass.config_entries.async_setup(config_entry.entry_id)
await _assert_http_api_responses(client, HTTPStatus.OK, HTTPStatus.BAD_REQUEST)
assert set(hass.data["frontend_panels"]) == {"calendar"}
assert set(hass.data["websocket_api"]) == {
"calendar/event/create",
"calendar/event/delete",
"calendar/event/update",
}
async def test_frontend_resources_registered_after_first_platform_setup(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
set_time_zone: None,
frozen_time: str | None,
mock_setup_platform_integration: None,
) -> None:
"""Test that frontend resources are registered after the first platform is set up."""
await async_setup_component(hass, "http", {})
client = await hass_client()
await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND)
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"}})
await _assert_http_api_responses(client, HTTPStatus.OK, HTTPStatus.BAD_REQUEST)
assert set(hass.data["frontend_panels"]) == {"calendar"}
assert set(hass.data["websocket_api"]) == {
"calendar/event/create",
"calendar/event/delete",
"calendar/event/update",
}

View File

@@ -19,7 +19,7 @@ async def mock_setup_dependencies(
recorder_mock: Recorder,
hass: HomeAssistant,
set_time_zone: None,
mock_setup_integration: None,
mock_setup_config_entry_integration: None,
config_entry: MockConfigEntry,
) -> None:
"""Fixture that ensures the recorder is setup in the right order."""

View File

@@ -279,7 +279,7 @@ def mock_test_entity(test_entities: list[MockCalendarEntity]) -> MockCalendarEnt
@pytest.fixture(name="setup_platform", autouse=True)
async def mock_setup_platform(
hass: HomeAssistant,
mock_setup_integration: None,
mock_setup_config_entry_integration: None,
config_entry: MockConfigEntry,
) -> None:
"""Fixture to setup platforms used in the test."""