From b13c2e3018af48b9334d11363fbeaa5e1bba349c Mon Sep 17 00:00:00 2001 From: Tomasz Date: Tue, 27 Jan 2026 09:52:07 +0100 Subject: [PATCH] Add initial_color property to CalendarEntity (#145606) Co-authored-by: Allen Porter --- homeassistant/components/calendar/__init__.py | 26 +++++++-- tests/components/calendar/conftest.py | 9 +++- tests/components/calendar/test_init.py | 53 +++++++++++++++++++ tests/components/calendar/test_trigger.py | 2 +- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 8f8d04a7c4c..6c34c8bf540 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -32,7 +32,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_time @@ -516,6 +516,26 @@ class CalendarEntity(Entity): _alarm_unsubs: list[CALLBACK_TYPE] | None = None + _attr_initial_color: str | None = None + + @property + def initial_color(self) -> str | None: + """Return the initial color for the calendar entity.""" + return self._attr_initial_color + + def get_initial_entity_options(self) -> er.EntityOptionsType | None: + """Return initial entity options.""" + if self.initial_color is None: + return None + + # Validate that it's a valid hex color string with # prefix + try: + validated_color = cv.color_hex(self.initial_color) + except vol.Invalid: + return None + + return {DOMAIN: {"color": validated_color}} + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" @@ -533,8 +553,8 @@ class CalendarEntity(Entity): "all_day": event.all_day, "start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT), "end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT), - "location": event.location if event.location else "", - "description": event.description if event.description else "", + "location": event.location or "", + "description": event.description or "", } @final diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 2226d66a3bc..2382f37783e 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -48,11 +48,13 @@ class MockCalendarEntity(CalendarEntity): self, name: str, events: list[CalendarEvent] | None = None, + initial_color: str | None = None, unique_id: str | None = None, ) -> None: """Initialize entity.""" self._attr_name = name.capitalize() self._events = events or [] + self._attr_initial_color = initial_color self._attr_unique_id = unique_id @property @@ -206,4 +208,9 @@ def create_test_entities() -> list[MockCalendarEntity]: ) entity2.async_get_events = AsyncMock(wraps=entity2.async_get_events) - return [entity1, entity2] + entity3 = MockCalendarEntity( + "Calendar 3", [], initial_color="#FF0000", unique_id="calendar_3" + ) + entity3.async_get_events = AsyncMock(wraps=entity3.async_get_events) + + return [entity1, entity2, entity3] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 6de0a7ef936..67eca34728e 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -16,6 +16,7 @@ import voluptuous as vol from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -123,6 +124,7 @@ async def test_calendars_http_api( assert data == [ {"entity_id": "calendar.calendar_1", "name": "Calendar 1"}, {"entity_id": "calendar.calendar_2", "name": "Calendar 2"}, + {"entity_id": "calendar.calendar_3", "name": "Calendar 3"}, ] @@ -604,3 +606,54 @@ async def test_list_events_service_same_dates( blocking=True, return_response=True, ) + + +async def test_calendar_initial_color_valid( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + test_entities: list[MockCalendarEntity], +) -> None: + """Test that initial_color creates initial entity options.""" + # Entity 3 was created with an initial_color + entity = test_entities[2] + + # Check that entity registry was populated with the initial_color + entry = entity_registry.async_get(entity.entity_id) + assert entry is not None + assert entry.options.get(DOMAIN, {}).get("color") == "#FF0000" + + +@pytest.mark.parametrize( + "invalid_initial_color", + [ + "FF0000", # Missing # + "#FF00", # Too short + "#FF00000", # Too long + "#GGGGGG", # Invalid hex + "red", # Not hex + "", # Empty + ], +) +async def test_calendar_initial_color_invalid( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + invalid_initial_color: str, +) -> None: + """Test that invalid initial_color is ignored.""" + entity = MockCalendarEntity( + "Invalid Color Test", + [], + initial_color=invalid_initial_color, + unique_id=f"test_{invalid_initial_color}", + ) + assert entity.get_initial_entity_options() is None + + +async def test_calendar_initial_color_none( + hass: HomeAssistant, + test_entities: list[MockCalendarEntity], +) -> None: + """Test that entities without initial_color return None.""" + # Entities 1 and 2 were created without an initial_color + entity = test_entities[0] + assert entity.get_initial_entity_options() is None diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 8d4fcab37d6..8435f43f930 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -705,7 +705,7 @@ async def test_legacy_entity_type( "action": TEST_AUTOMATION_ACTION, "trigger": { "platform": calendar.DOMAIN, - "entity_id": "calendar.calendar_3", + "entity_id": "calendar.calendar_4", }, } },