diff --git a/homeassistant/components/workday/calendar.py b/homeassistant/components/workday/calendar.py new file mode 100644 index 00000000000..57aa9f99bfb --- /dev/null +++ b/homeassistant/components/workday/calendar.py @@ -0,0 +1,98 @@ +"""Workday Calendar.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +from holidays import HolidayBase + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import WorkdayConfigEntry +from .const import CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS +from .entity import BaseWorkdayEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: WorkdayConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Holiday Calendar config entry.""" + days_offset: int = int(entry.options[CONF_OFFSET]) + excludes: list[str] = entry.options[CONF_EXCLUDES] + sensor_name: str = entry.options[CONF_NAME] + workdays: list[str] = entry.options[CONF_WORKDAYS] + obj_holidays = entry.runtime_data + + async_add_entities( + [ + WorkdayCalendarEntity( + obj_holidays, + workdays, + excludes, + days_offset, + sensor_name, + entry.entry_id, + ) + ], + ) + + +class WorkdayCalendarEntity(BaseWorkdayEntity, CalendarEntity): + """Representation of a Wokrday Calendar.""" + + def __init__( + self, + obj_holidays: HolidayBase, + workdays: list[str], + excludes: list[str], + days_offset: int, + name: str, + entry_id: str, + ) -> None: + """Initialize WorkdayCalendarEntity.""" + super().__init__( + obj_holidays, + workdays, + excludes, + days_offset, + name, + entry_id, + ) + self._attr_unique_id = f"{entry_id}-calender" + self._attr_event = None + self.event_list: list[CalendarEvent] = [] + self._name = name + + def update_data(self, now: datetime) -> None: + """Update data.""" + event_list = [] + for i in range(365): + future_date = now.date() + timedelta(days=i) + if self.date_is_workday(future_date): + event = CalendarEvent( + summary=self._name, + start=future_date, + end=future_date, + ) + event_list.append(event) + self.event_list = event_list + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return ( + sorted(self.event_list, key=lambda e: e.start)[0] + if self.event_list + else None + ) + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + return self.event_list diff --git a/homeassistant/components/workday/const.py b/homeassistant/components/workday/const.py index 76580ae642f..e8a6656d9e2 100644 --- a/homeassistant/components/workday/const.py +++ b/homeassistant/components/workday/const.py @@ -11,7 +11,7 @@ LOGGER = logging.getLogger(__package__) ALLOWED_DAYS = [*WEEKDAYS, "holiday"] DOMAIN = "workday" -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR] CONF_PROVINCE = "province" CONF_WORKDAYS = "workdays" diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index feedc52331b..e78ece25c21 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -212,6 +212,11 @@ } } } + }, + "calendar": { + "workday": { + "name": "[%key:component::calendar::title%]" + } } }, "services": { diff --git a/tests/components/workday/test_calendar.py b/tests/components/workday/test_calendar.py new file mode 100644 index 00000000000..1ddf4611b35 --- /dev/null +++ b/tests/components/workday/test_calendar.py @@ -0,0 +1,79 @@ +"""Tests for calendar platform of Workday integration.""" + +from datetime import datetime, timedelta + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.calendar import ( + DOMAIN as CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import TEST_CONFIG_WITH_PROVINCE, init_integration + +from tests.common import async_fire_time_changed + + +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_holiday_calendar_entity( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + time_zone: str, +) -> None: + """Test HolidayCalendarEntity functionality.""" + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 0, 1, 1, tzinfo=zone)) # New Years Day + await init_integration(hass, TEST_CONFIG_WITH_PROVINCE) + + 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.workday_sensor_calendar", + "end_date_time": dt_util.now() + timedelta(hours=1), + }, + blocking=True, + return_response=True, + ) + assert { + "end": "2023-01-01", + "start": "2023-01-02", + "summary": "Workday Sensor", + } not in response["calendar.workday_sensor_calendar"]["events"] + assert { + "end": "2023-01-04", + "start": "2023-01-03", + "summary": "Workday Sensor", + } in response["calendar.workday_sensor_calendar"]["events"] + + state = hass.states.get("calendar.workday_sensor_calendar") + assert state is not None + assert state.state == "off" + + freezer.move_to( + datetime(2023, 1, 2, 0, 1, 1, tzinfo=zone) + ) # Day after New Years Day + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("calendar.workday_sensor_calendar") + assert state is not None + assert state.state == "on" + + freezer.move_to(datetime(2023, 1, 7, 0, 1, 1, tzinfo=zone)) # Workday + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("calendar.workday_sensor_calendar") + assert state is not None + assert state.state == "off"