Allow to add optional holiday categories in workday (#121396)
* Allow to add optional holiday categories in workday * Add tests * Fix coverage
This commit is contained in:
parent
dab66990c0
commit
0cde518a89
7 changed files with 185 additions and 23 deletions
|
@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta
|
|||
from typing import Final
|
||||
|
||||
from holidays import (
|
||||
PUBLIC,
|
||||
HolidayBase,
|
||||
__version__ as python_holidays_version,
|
||||
country_holidays,
|
||||
|
@ -35,6 +36,7 @@ from homeassistant.util import dt as dt_util, slugify
|
|||
from .const import (
|
||||
ALLOWED_DAYS,
|
||||
CONF_ADD_HOLIDAYS,
|
||||
CONF_CATEGORY,
|
||||
CONF_EXCLUDES,
|
||||
CONF_OFFSET,
|
||||
CONF_PROVINCE,
|
||||
|
@ -69,17 +71,28 @@ def validate_dates(holiday_list: list[str]) -> list[str]:
|
|||
|
||||
|
||||
def _get_obj_holidays(
|
||||
country: str | None, province: str | None, year: int, language: str | None
|
||||
country: str | None,
|
||||
province: str | None,
|
||||
year: int,
|
||||
language: str | None,
|
||||
categories: list[str] | None,
|
||||
) -> HolidayBase:
|
||||
"""Get the object for the requested country and year."""
|
||||
if not country:
|
||||
return HolidayBase()
|
||||
|
||||
set_categories = None
|
||||
if categories:
|
||||
category_list = [PUBLIC]
|
||||
category_list.extend(categories)
|
||||
set_categories = tuple(category_list)
|
||||
|
||||
obj_holidays: HolidayBase = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=language,
|
||||
categories=set_categories, # type: ignore[arg-type]
|
||||
)
|
||||
if (supported_languages := obj_holidays.supported_languages) and language == "en":
|
||||
for lang in supported_languages:
|
||||
|
@ -89,6 +102,7 @@ def _get_obj_holidays(
|
|||
subdiv=province,
|
||||
years=year,
|
||||
language=lang,
|
||||
categories=set_categories, # type: ignore[arg-type]
|
||||
)
|
||||
LOGGER.debug("Changing language from %s to %s", language, lang)
|
||||
return obj_holidays
|
||||
|
@ -107,10 +121,11 @@ async def async_setup_entry(
|
|||
sensor_name: str = entry.options[CONF_NAME]
|
||||
workdays: list[str] = entry.options[CONF_WORKDAYS]
|
||||
language: str | None = entry.options.get(CONF_LANGUAGE)
|
||||
categories: list[str] | None = entry.options.get(CONF_CATEGORY)
|
||||
|
||||
year: int = (dt_util.now() + timedelta(days=days_offset)).year
|
||||
obj_holidays: HolidayBase = await hass.async_add_executor_job(
|
||||
_get_obj_holidays, country, province, year, language
|
||||
_get_obj_holidays, country, province, year, language, categories
|
||||
)
|
||||
calc_add_holidays: list[str] = validate_dates(add_holidays)
|
||||
calc_remove_holidays: list[str] = validate_dates(remove_holidays)
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from holidays import HolidayBase, country_holidays, list_supported_countries
|
||||
from holidays import PUBLIC, HolidayBase, country_holidays, list_supported_countries
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
|
@ -36,6 +36,7 @@ from homeassistant.util import dt as dt_util
|
|||
from .const import (
|
||||
ALLOWED_DAYS,
|
||||
CONF_ADD_HOLIDAYS,
|
||||
CONF_CATEGORY,
|
||||
CONF_EXCLUDES,
|
||||
CONF_OFFSET,
|
||||
CONF_PROVINCE,
|
||||
|
@ -86,7 +87,29 @@ def add_province_and_language_to_schema(
|
|||
),
|
||||
}
|
||||
|
||||
return vol.Schema({**DATA_SCHEMA_OPT.schema, **language_schema, **province_schema})
|
||||
category_schema = {}
|
||||
# PUBLIC will always be included and can therefore not be set/removed
|
||||
_categories = [x for x in _country.supported_categories if x != PUBLIC]
|
||||
if _categories:
|
||||
category_schema = {
|
||||
vol.Optional(CONF_CATEGORY): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=_categories,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
multiple=True,
|
||||
translation_key=CONF_CATEGORY,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
**DATA_SCHEMA_OPT.schema,
|
||||
**language_schema,
|
||||
**province_schema,
|
||||
**category_schema,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_valid_date_range(check_date: str, error: type[HomeAssistantError]) -> bool:
|
||||
|
@ -256,6 +279,8 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
|
||||
CONF_PROVINCE: combined_input.get(CONF_PROVINCE),
|
||||
}
|
||||
if CONF_CATEGORY in combined_input:
|
||||
abort_match[CONF_CATEGORY] = combined_input[CONF_CATEGORY]
|
||||
LOGGER.debug("abort_check in options with %s", combined_input)
|
||||
self._async_abort_entries_match(abort_match)
|
||||
|
||||
|
@ -314,18 +339,19 @@ class WorkdayOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||
errors["remove_holidays"] = "remove_holiday_range_error"
|
||||
else:
|
||||
LOGGER.debug("abort_check in options with %s", combined_input)
|
||||
abort_match = {
|
||||
CONF_COUNTRY: self._config_entry.options.get(CONF_COUNTRY),
|
||||
CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
|
||||
CONF_OFFSET: combined_input[CONF_OFFSET],
|
||||
CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
|
||||
CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
|
||||
CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
|
||||
CONF_PROVINCE: combined_input.get(CONF_PROVINCE),
|
||||
}
|
||||
if CONF_CATEGORY in combined_input:
|
||||
abort_match[CONF_CATEGORY] = combined_input[CONF_CATEGORY]
|
||||
try:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_COUNTRY: self._config_entry.options.get(CONF_COUNTRY),
|
||||
CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
|
||||
CONF_OFFSET: combined_input[CONF_OFFSET],
|
||||
CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
|
||||
CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
|
||||
CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
|
||||
CONF_PROVINCE: combined_input.get(CONF_PROVINCE),
|
||||
}
|
||||
)
|
||||
self._async_abort_entries_match(abort_match)
|
||||
except AbortFlow as err:
|
||||
errors = {"base": err.reason}
|
||||
else:
|
||||
|
|
|
@ -19,6 +19,7 @@ CONF_EXCLUDES = "excludes"
|
|||
CONF_OFFSET = "days_offset"
|
||||
CONF_ADD_HOLIDAYS = "add_holidays"
|
||||
CONF_REMOVE_HOLIDAYS = "remove_holidays"
|
||||
CONF_CATEGORY = "category"
|
||||
|
||||
# By default, Monday - Friday are workdays
|
||||
DEFAULT_WORKDAYS = ["mon", "tue", "wed", "thu", "fri"]
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"add_holidays": "Add holidays",
|
||||
"remove_holidays": "Remove Holidays",
|
||||
"province": "Subdivision of country",
|
||||
"language": "Language for named holidays"
|
||||
"language": "Language for named holidays",
|
||||
"category": "Additional category as holiday"
|
||||
},
|
||||
"data_description": {
|
||||
"excludes": "List of workdays to exclude, notice the keyword `holiday` and read the documentation on how to use it correctly",
|
||||
|
@ -29,7 +30,8 @@
|
|||
"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 or region of country",
|
||||
"language": "Language to use when configuring named holiday exclusions"
|
||||
"language": "Language to use when configuring named holiday exclusions",
|
||||
"category": "Select additional categories to include as holidays"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -51,7 +53,8 @@
|
|||
"add_holidays": "[%key:component::workday::config::step::options::data::add_holidays%]",
|
||||
"remove_holidays": "[%key:component::workday::config::step::options::data::remove_holidays%]",
|
||||
"province": "[%key:component::workday::config::step::options::data::province%]",
|
||||
"language": "[%key:component::workday::config::step::options::data::language%]"
|
||||
"language": "[%key:component::workday::config::step::options::data::language%]",
|
||||
"category": "[%key:component::workday::config::step::options::data::category%]"
|
||||
},
|
||||
"data_description": {
|
||||
"excludes": "[%key:component::workday::config::step::options::data_description::excludes%]",
|
||||
|
@ -60,7 +63,8 @@
|
|||
"add_holidays": "[%key:component::workday::config::step::options::data_description::add_holidays%]",
|
||||
"remove_holidays": "[%key:component::workday::config::step::options::data_description::remove_holidays%]",
|
||||
"province": "[%key:component::workday::config::step::options::data_description::province%]",
|
||||
"language": "[%key:component::workday::config::step::options::data_description::language%]"
|
||||
"language": "[%key:component::workday::config::step::options::data_description::language%]",
|
||||
"category": "[%key:component::workday::config::step::options::data_description::category%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -78,6 +82,24 @@
|
|||
"none": "No subdivision"
|
||||
}
|
||||
},
|
||||
"category": {
|
||||
"options": {
|
||||
"armed_forces": "Armed forces",
|
||||
"bank": "Bank",
|
||||
"government": "Government",
|
||||
"half_day": "Half day",
|
||||
"optional": "Optional",
|
||||
"public": "Public",
|
||||
"school": "School",
|
||||
"unofficial": "Unofficial",
|
||||
"workday": "Workday",
|
||||
"chinese": "Chinese",
|
||||
"christian": "Christian",
|
||||
"hebrew": "Hebrew",
|
||||
"hindu": "Hindu",
|
||||
"islamic": "Islamic"
|
||||
}
|
||||
},
|
||||
"days": {
|
||||
"options": {
|
||||
"mon": "[%key:common::time::monday%]",
|
||||
|
|
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from holidays import OPTIONAL
|
||||
|
||||
from homeassistant.components.workday.const import (
|
||||
DEFAULT_EXCLUDES,
|
||||
DEFAULT_NAME,
|
||||
|
@ -310,3 +312,26 @@ TEST_LANGUAGE_NO_CHANGE = {
|
|||
"remove_holidays": ["2022-12-04", "2022-12-24,2022-12-26"],
|
||||
"language": "de",
|
||||
}
|
||||
TEST_NO_OPTIONAL_CATEGORY = {
|
||||
"name": DEFAULT_NAME,
|
||||
"country": "CH",
|
||||
"province": "FR",
|
||||
"excludes": DEFAULT_EXCLUDES,
|
||||
"days_offset": DEFAULT_OFFSET,
|
||||
"workdays": DEFAULT_WORKDAYS,
|
||||
"add_holidays": [],
|
||||
"remove_holidays": [],
|
||||
"language": "de",
|
||||
}
|
||||
TEST_OPTIONAL_CATEGORY = {
|
||||
"name": DEFAULT_NAME,
|
||||
"country": "CH",
|
||||
"province": "FR",
|
||||
"excludes": DEFAULT_EXCLUDES,
|
||||
"days_offset": DEFAULT_OFFSET,
|
||||
"workdays": DEFAULT_WORKDAYS,
|
||||
"add_holidays": [],
|
||||
"remove_holidays": [],
|
||||
"language": "de",
|
||||
"category": [OPTIONAL],
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ from . import (
|
|||
TEST_CONFIG_YESTERDAY,
|
||||
TEST_LANGUAGE_CHANGE,
|
||||
TEST_LANGUAGE_NO_CHANGE,
|
||||
TEST_NO_OPTIONAL_CATEGORY,
|
||||
TEST_OPTIONAL_CATEGORY,
|
||||
init_integration,
|
||||
)
|
||||
|
||||
|
@ -400,3 +402,23 @@ async def test_language_difference_no_change_other_language(
|
|||
"""Test skipping if no difference in language naming."""
|
||||
await init_integration(hass, TEST_LANGUAGE_NO_CHANGE)
|
||||
assert "Changing language from en to en_US" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "end_state"),
|
||||
[(TEST_OPTIONAL_CATEGORY, "off"), (TEST_NO_OPTIONAL_CATEGORY, "on")],
|
||||
)
|
||||
async def test_optional_category(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
end_state: str,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test setup from various configs."""
|
||||
# CH, subdiv FR has optional holiday Jan 2nd
|
||||
freezer.move_to(datetime(2024, 1, 2, 12, tzinfo=UTC)) # Tuesday
|
||||
await init_integration(hass, config)
|
||||
|
||||
state = hass.states.get("binary_sensor.workday_sensor")
|
||||
assert state is not None
|
||||
assert state.state == end_state
|
||||
|
|
|
@ -5,11 +5,13 @@ from __future__ import annotations
|
|||
from datetime import datetime
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from holidays import HALF_DAY, OPTIONAL
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.workday.const import (
|
||||
CONF_ADD_HOLIDAYS,
|
||||
CONF_CATEGORY,
|
||||
CONF_EXCLUDES,
|
||||
CONF_OFFSET,
|
||||
CONF_REMOVE_HOLIDAYS,
|
||||
|
@ -354,13 +356,14 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None:
|
|||
hass,
|
||||
{
|
||||
"name": "Workday Sensor",
|
||||
"country": "DE",
|
||||
"country": "CH",
|
||||
"excludes": ["sat", "sun", "holiday"],
|
||||
"days_offset": 0,
|
||||
"workdays": ["mon", "tue", "wed", "thu", "fri"],
|
||||
"add_holidays": [],
|
||||
"remove_holidays": [],
|
||||
"province": None,
|
||||
"province": "FR",
|
||||
"category": [OPTIONAL],
|
||||
},
|
||||
entry_id="1",
|
||||
)
|
||||
|
@ -368,13 +371,14 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None:
|
|||
hass,
|
||||
{
|
||||
"name": "Workday Sensor2",
|
||||
"country": "DE",
|
||||
"country": "CH",
|
||||
"excludes": ["sat", "sun", "holiday"],
|
||||
"days_offset": 0,
|
||||
"workdays": ["mon", "tue", "wed", "thu", "fri"],
|
||||
"add_holidays": ["2023-03-28"],
|
||||
"remove_holidays": [],
|
||||
"province": None,
|
||||
"province": "FR",
|
||||
"category": [OPTIONAL],
|
||||
},
|
||||
entry_id="2",
|
||||
)
|
||||
|
@ -389,6 +393,8 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None:
|
|||
"workdays": ["mon", "tue", "wed", "thu", "fri"],
|
||||
"add_holidays": [],
|
||||
"remove_holidays": [],
|
||||
"province": "FR",
|
||||
"category": [OPTIONAL],
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -602,3 +608,48 @@ async def test_language(
|
|||
state = hass.states.get("binary_sensor.workday_sensor")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
|
||||
|
||||
async def test_form_with_categories(hass: HomeAssistant) -> None:
|
||||
"""Test optional categories."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "Workday Sensor",
|
||||
CONF_COUNTRY: "CH",
|
||||
},
|
||||
)
|
||||
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: [],
|
||||
CONF_REMOVE_HOLIDAYS: [],
|
||||
CONF_LANGUAGE: "de",
|
||||
CONF_CATEGORY: [HALF_DAY],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "Workday Sensor"
|
||||
assert result3["options"] == {
|
||||
"name": "Workday Sensor",
|
||||
"country": "CH",
|
||||
"excludes": ["sat", "sun", "holiday"],
|
||||
"days_offset": 0,
|
||||
"workdays": ["mon", "tue", "wed", "thu", "fri"],
|
||||
"add_holidays": [],
|
||||
"remove_holidays": [],
|
||||
"language": "de",
|
||||
"category": ["half_day"],
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue