Add date range to Workday (#96255)

pull/100899/head
G Johansson 2023-09-26 08:21:36 +02:00 committed by GitHub
parent 9c1944f830
commit 7b1b189f3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 319 additions and 11 deletions

View File

@ -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):

View File

@ -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."""

View File

@ -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"
}
},

View File

@ -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"],
}

View File

@ -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"

View File

@ -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",
}