Add date range to Workday (#96255)
parent
9c1944f830
commit
7b1b189f3e
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue