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:
G Johansson 2024-07-19 17:49:27 +02:00 committed by GitHub
parent dab66990c0
commit 0cde518a89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 185 additions and 23 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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