From 7b1b189f3ef569560f39150f3444498644dbdee6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 26 Sep 2023 08:21:36 +0200 Subject: [PATCH] Add date range to Workday (#96255) --- .../components/workday/binary_sensor.py | 30 +++- .../components/workday/config_flow.py | 41 ++++- homeassistant/components/workday/strings.json | 10 +- tests/components/workday/__init__.py | 50 ++++++ .../components/workday/test_binary_sensor.py | 55 +++++++ tests/components/workday/test_config_flow.py | 144 ++++++++++++++++++ 6 files changed, 319 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index b60346c3bbb..5daea6ce129 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -5,7 +5,6 @@ from datetime import date, timedelta from typing import Any from holidays import ( - DateLike, HolidayBase, __version__ as python_holidays_version, country_holidays, @@ -45,6 +44,26 @@ from .const import ( ) +def validate_dates(holiday_list: list[str]) -> list[str]: + """Validate and adds to list of dates to add or remove.""" + calc_holidays: list[str] = [] + for add_date in holiday_list: + if add_date.find(",") > 0: + dates = add_date.split(",", maxsplit=1) + d1 = dt_util.parse_date(dates[0]) + d2 = dt_util.parse_date(dates[1]) + if d1 is None or d2 is None: + LOGGER.error("Incorrect dates in date range: %s", add_date) + continue + _range: timedelta = d2 - d1 + for i in range(_range.days + 1): + day = d1 + timedelta(days=i) + calc_holidays.append(day.strftime("%Y-%m-%d")) + continue + calc_holidays.append(add_date) + return calc_holidays + + def valid_country(value: Any) -> str: """Validate that the given country is supported.""" value = cv.string(value) @@ -119,7 +138,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Workday sensor.""" - add_holidays: list[DateLike] = entry.options[CONF_ADD_HOLIDAYS] + add_holidays: list[str] = entry.options[CONF_ADD_HOLIDAYS] remove_holidays: list[str] = entry.options[CONF_REMOVE_HOLIDAYS] country: str | None = entry.options.get(CONF_COUNTRY) days_offset: int = int(entry.options[CONF_OFFSET]) @@ -141,14 +160,17 @@ async def async_setup_entry( else: obj_holidays = HolidayBase() + calc_add_holidays: list[str] = validate_dates(add_holidays) + calc_remove_holidays: list[str] = validate_dates(remove_holidays) + # Add custom holidays try: - obj_holidays.append(add_holidays) + obj_holidays.append(calc_add_holidays) # type: ignore[arg-type] except ValueError as error: LOGGER.error("Could not add custom holidays: %s", error) # Remove holidays - for remove_holiday in remove_holidays: + for remove_holiday in calc_remove_holidays: try: # is this formatted as a date? if dt_util.parse_date(remove_holiday): diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index df74fff83e1..6be7e119876 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -69,10 +69,24 @@ def add_province_to_schema( return vol.Schema({**DATA_SCHEMA_OPT.schema, **add_schema}) +def _is_valid_date_range(check_date: str, error: type[HomeAssistantError]) -> bool: + """Validate date range.""" + if check_date.find(",") > 0: + dates = check_date.split(",", maxsplit=1) + for date in dates: + if dt_util.parse_date(date) is None: + raise error("Incorrect date in range") + return True + return False + + def validate_custom_dates(user_input: dict[str, Any]) -> None: """Validate custom dates for add/remove holidays.""" for add_date in user_input[CONF_ADD_HOLIDAYS]: - if dt_util.parse_date(add_date) is None: + if ( + not _is_valid_date_range(add_date, AddDateRangeError) + and dt_util.parse_date(add_date) is None + ): raise AddDatesError("Incorrect date") year: int = dt_util.now().year @@ -88,9 +102,12 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: obj_holidays = HolidayBase(years=year) for remove_date in user_input[CONF_REMOVE_HOLIDAYS]: - if dt_util.parse_date(remove_date) is None: - if obj_holidays.get_named(remove_date) == []: - raise RemoveDatesError("Incorrect date or name") + if ( + not _is_valid_date_range(remove_date, RemoveDateRangeError) + and dt_util.parse_date(remove_date) is None + and obj_holidays.get_named(remove_date) == [] + ): + raise RemoveDatesError("Incorrect date or name") DATA_SCHEMA_SETUP = vol.Schema( @@ -223,8 +240,12 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN): ) except AddDatesError: errors["add_holidays"] = "add_holiday_error" + except AddDateRangeError: + errors["add_holidays"] = "add_holiday_range_error" except RemoveDatesError: errors["remove_holidays"] = "remove_holiday_error" + except RemoveDateRangeError: + errors["remove_holidays"] = "remove_holiday_range_error" except NotImplementedError: self.async_abort(reason="incorrect_province") @@ -284,8 +305,12 @@ class WorkdayOptionsFlowHandler(OptionsFlowWithConfigEntry): ) except AddDatesError: errors["add_holidays"] = "add_holiday_error" + except AddDateRangeError: + errors["add_holidays"] = "add_holiday_range_error" except RemoveDatesError: errors["remove_holidays"] = "remove_holiday_error" + except RemoveDateRangeError: + errors["remove_holidays"] = "remove_holiday_range_error" else: LOGGER.debug("abort_check in options with %s", combined_input) try: @@ -328,9 +353,17 @@ class AddDatesError(HomeAssistantError): """Exception for error adding dates.""" +class AddDateRangeError(HomeAssistantError): + """Exception for error adding dates.""" + + class RemoveDatesError(HomeAssistantError): """Exception for error removing dates.""" +class RemoveDateRangeError(HomeAssistantError): + """Exception for error removing dates.""" + + class CountryNotExist(HomeAssistantError): """Exception country does not exist error.""" diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 718f99d7c8a..a4c2baf31c8 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -26,15 +26,17 @@ "excludes": "List of workdays to exclude", "days_offset": "Days offset", "workdays": "List of workdays", - "add_holidays": "Add custom holidays as YYYY-MM-DD", - "remove_holidays": "Remove holidays as YYYY-MM-DD or by using partial of name", + "add_holidays": "Add custom holidays as YYYY-MM-DD or as range using `,` as separator", + "remove_holidays": "Remove holidays as YYYY-MM-DD, as range using `,` as separator or by using partial of name", "province": "State, Territory, Province, Region of Country" } } }, "error": { "add_holiday_error": "Incorrect format on date (YYYY-MM-DD)", - "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found" + "add_holiday_range_error": "Incorrect format on date range (YYYY-MM-DD,YYYY-MM-DD)", + "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found", + "remove_holiday_range_error": "Incorrect format on date range (YYYY-MM-DD,YYYY-MM-DD)" } }, "options": { @@ -61,7 +63,9 @@ }, "error": { "add_holiday_error": "[%key:component::workday::config::error::add_holiday_error%]", + "add_holiday_range_error": "[%key:component::workday::config::error::add_holiday_range_error%]", "remove_holiday_error": "[%key:component::workday::config::error::remove_holiday_error%]", + "remove_holiday_range_error": "[%key:component::workday::config::error::remove_holiday_range_error%]", "already_configured": "Service with this configuration already exist" } }, diff --git a/tests/components/workday/__init__.py b/tests/components/workday/__init__.py index 2a1b61a0a0f..f9e44359b00 100644 --- a/tests/components/workday/__init__.py +++ b/tests/components/workday/__init__.py @@ -197,3 +197,53 @@ TEST_CONFIG_INCORRECT_ADD_REMOVE = { "add_holidays": ["2023-12-32"], "remove_holidays": ["2023-12-32"], } +TEST_CONFIG_INCORRECT_ADD_DATE_RANGE = { + "name": DEFAULT_NAME, + "country": "DE", + "province": "BW", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": ["2023-12-01", "2023-12-30,2023-12-32"], + "remove_holidays": [], +} +TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE = { + "name": DEFAULT_NAME, + "country": "DE", + "province": "BW", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": [], + "remove_holidays": ["2023-12-25", "2023-12-30,2023-12-32"], +} +TEST_CONFIG_INCORRECT_ADD_DATE_RANGE_LEN = { + "name": DEFAULT_NAME, + "country": "DE", + "province": "BW", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": ["2023-12-01", "2023-12-29,2023-12-30,2023-12-31"], + "remove_holidays": [], +} +TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE_LEN = { + "name": DEFAULT_NAME, + "country": "DE", + "province": "BW", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": [], + "remove_holidays": ["2023-12-25", "2023-12-29,2023-12-30,2023-12-31"], +} +TEST_CONFIG_ADD_REMOVE_DATE_RANGE = { + "name": DEFAULT_NAME, + "country": "DE", + "province": "BW", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": ["2022-12-01", "2022-12-05,2022-12-15"], + "remove_holidays": ["2022-12-04", "2022-12-24,2022-12-26"], +} diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index a3923bfb291..5c387e9a179 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -12,13 +12,18 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC from . import ( + TEST_CONFIG_ADD_REMOVE_DATE_RANGE, TEST_CONFIG_DAY_AFTER_TOMORROW, TEST_CONFIG_EXAMPLE_1, TEST_CONFIG_EXAMPLE_2, TEST_CONFIG_INCLUDE_HOLIDAY, + TEST_CONFIG_INCORRECT_ADD_DATE_RANGE, + TEST_CONFIG_INCORRECT_ADD_DATE_RANGE_LEN, TEST_CONFIG_INCORRECT_ADD_REMOVE, TEST_CONFIG_INCORRECT_COUNTRY, TEST_CONFIG_INCORRECT_PROVINCE, + TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE, + TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE_LEN, TEST_CONFIG_NO_COUNTRY, TEST_CONFIG_NO_COUNTRY_ADD_HOLIDAY, TEST_CONFIG_NO_PROVINCE, @@ -264,3 +269,53 @@ async def test_setup_incorrect_add_remove( in caplog.text ) assert "No holiday found matching '2023-12-32'" in caplog.text + + +async def test_setup_incorrect_add_holiday_ranges( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup with incorrect add/remove holiday ranges.""" + freezer.move_to(datetime(2017, 1, 6, 12, tzinfo=UTC)) # Friday + await init_integration(hass, TEST_CONFIG_INCORRECT_ADD_DATE_RANGE) + await init_integration(hass, TEST_CONFIG_INCORRECT_ADD_DATE_RANGE_LEN, "2") + + hass.states.get("binary_sensor.workday_sensor") + + assert "Incorrect dates in date range: 2023-12-30,2023-12-32" in caplog.text + assert ( + "Incorrect dates in date range: 2023-12-29,2023-12-30,2023-12-31" in caplog.text + ) + + +async def test_setup_incorrect_remove_holiday_ranges( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup with incorrect add/remove holiday ranges.""" + freezer.move_to(datetime(2017, 1, 6, 12, tzinfo=UTC)) # Friday + await init_integration(hass, TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE) + await init_integration(hass, TEST_CONFIG_INCORRECT_REMOVE_DATE_RANGE_LEN, "2") + + hass.states.get("binary_sensor.workday_sensor") + + assert "Incorrect dates in date range: 2023-12-30,2023-12-32" in caplog.text + assert ( + "Incorrect dates in date range: 2023-12-29,2023-12-30,2023-12-31" in caplog.text + ) + + +async def test_setup_date_range( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test setup with date range.""" + freezer.move_to( + datetime(2022, 12, 26, 12, tzinfo=UTC) + ) # Boxing Day should be working day + await init_integration(hass, TEST_CONFIG_ADD_REMOVE_DATE_RANGE) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state.state == "on" diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index 78cbbf97fed..65e6c70fa00 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -528,3 +528,147 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} + + +async def test_form_incorrect_date_range(hass: HomeAssistant) -> None: + """Test errors in setup entry.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "Workday Sensor", + CONF_COUNTRY: "DE", + }, + ) + await hass.async_block_till_done() + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: ["2022-12-12", "2022-12-30,2022-12-32"], + CONF_REMOVE_HOLIDAYS: [], + CONF_PROVINCE: "none", + }, + ) + await hass.async_block_till_done() + assert result3["errors"] == {"add_holidays": "add_holiday_range_error"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: ["2022-12-12"], + CONF_REMOVE_HOLIDAYS: ["2022-12-25", "2022-12-30,2022-12-32"], + CONF_PROVINCE: "none", + }, + ) + await hass.async_block_till_done() + + assert result3["errors"] == {"remove_holidays": "remove_holiday_range_error"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: ["2022-12-12", "2022-12-01,2022-12-10"], + CONF_REMOVE_HOLIDAYS: ["2022-12-25", "2022-12-30,2022-12-31"], + CONF_PROVINCE: "none", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Workday Sensor" + assert result3["options"] == { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": ["2022-12-12", "2022-12-01,2022-12-10"], + "remove_holidays": ["2022-12-25", "2022-12-30,2022-12-31"], + "province": None, + } + + +async def test_options_form_incorrect_date_ranges(hass: HomeAssistant) -> None: + """Test errors in options.""" + + entry = await init_integration( + hass, + { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "province": None, + }, + ) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": ["2022-12-30,2022-12-32"], + "remove_holidays": [], + "province": "BW", + }, + ) + + assert result2["errors"] == {"add_holidays": "add_holiday_range_error"} + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": ["2022-12-30,2022-12-31"], + "remove_holidays": ["2022-13-25,2022-12-26"], + "province": "BW", + }, + ) + + assert result2["errors"] == {"remove_holidays": "remove_holiday_range_error"} + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": ["2022-12-30,2022-12-31"], + "remove_holidays": ["2022-12-25,2022-12-26"], + "province": "BW", + }, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": ["2022-12-30,2022-12-31"], + "remove_holidays": ["2022-12-25,2022-12-26"], + "province": "BW", + }