From 1c4fbc658dc62e5cb99cf15cb05924f26573155e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 20 Jul 2025 11:09:04 +0000 Subject: [PATCH] Mods --- homeassistant/components/workday/__init__.py | 119 ++++++++++++++++-- .../components/workday/binary_sensor.py | 114 +---------------- homeassistant/components/workday/util.py | 27 ++++ 3 files changed, 141 insertions(+), 119 deletions(-) create mode 100644 homeassistant/components/workday/util.py diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index b2ad878af99..b7eef03f349 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta from functools import partial +from typing import cast -from holidays import PUBLIC, HolidayBase, country_holidays +from holidays import PUBLIC, DateLike, HolidayBase, country_holidays from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE @@ -13,9 +14,19 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.setup import SetupPhases, async_pause_setup -from homeassistant.util import dt as dt_util +from homeassistant.util import dt as dt_util, slugify -from .const import CONF_CATEGORY, CONF_OFFSET, CONF_PROVINCE, DOMAIN, LOGGER, PLATFORMS +from .const import ( + CONF_ADD_HOLIDAYS, + CONF_CATEGORY, + CONF_OFFSET, + CONF_PROVINCE, + CONF_REMOVE_HOLIDAYS, + DOMAIN, + LOGGER, + PLATFORMS, +) +from .util import validate_dates type WorkdayConfigEntry = ConfigEntry[HolidayBase] @@ -160,15 +171,97 @@ def _get_obj_holidays( return obj_holidays +def add_remove_custom_holidays( + hass: HomeAssistant, + entry: WorkdayConfigEntry, + country: str | None, + calc_add_holidays: list[DateLike], + calc_remove_holidays: list[str], +) -> None: + """Add or remove custom holidays.""" + next_year = dt_util.now().year + 1 + + # Add custom holidays + try: + entry.runtime_data.append(calc_add_holidays) + except ValueError as error: + LOGGER.error("Could not add custom holidays: %s", error) + + # Remove custom holidays + for remove_holiday in calc_remove_holidays: + try: + # is this formatted as a date? + if dt_util.parse_date(remove_holiday): + # remove holiday by date + removed = entry.runtime_data.pop(remove_holiday) + LOGGER.debug("Removed %s", remove_holiday) + else: + # remove holiday by name + LOGGER.debug("Treating '%s' as named holiday", remove_holiday) + removed = entry.runtime_data.pop_named(remove_holiday) + for holiday in removed: + LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) + except KeyError as unmatched: + LOGGER.warning("No holiday found matching %s", unmatched) + if _date := dt_util.parse_date(remove_holiday): + if _date.year <= next_year: + # Only check and raise issues for max next year + async_create_issue( + hass, + DOMAIN, + f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_date_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_named_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> bool: """Set up Workday from a config entry.""" - country: str | None = entry.options.get(CONF_COUNTRY) - province: str | None = entry.options.get(CONF_PROVINCE) - days_offset: int = int(entry.options[CONF_OFFSET]) - year: int = (dt_util.now() + timedelta(days=days_offset)).year - language: str | None = entry.options.get(CONF_LANGUAGE) + calc_add_holidays = cast( + list[DateLike], validate_dates(entry.options[CONF_ADD_HOLIDAYS]) + ) + calc_remove_holidays: list[str] = validate_dates( + entry.options[CONF_REMOVE_HOLIDAYS] + ) categories: list[str] | None = entry.options.get(CONF_CATEGORY) + country: str | None = entry.options.get(CONF_COUNTRY) + days_offset: int = int(entry.options[CONF_OFFSET]) + language: str | None = entry.options.get(CONF_LANGUAGE) + province: str | None = entry.options.get(CONF_PROVINCE) + year: int = (dt_util.now() + timedelta(days=days_offset)).year await _async_validate_country_and_province(hass, entry, country, province) @@ -176,6 +269,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> b country, province, year, language, categories ) + add_remove_custom_holidays( + hass, entry, country, calc_add_holidays, calc_remove_holidays + ) + + LOGGER.debug("Found the following holidays for your configuration:") + for holiday_date, name in sorted(entry.runtime_data.items()): + # Make explicit str variable to avoid "Incompatible types in assignment" + _holiday_string = holiday_date.strftime("%Y-%m-%d") + LOGGER.debug("%s %s", _holiday_string, name) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 4327bcfafbc..d8dcea5d4c9 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -9,7 +9,7 @@ from holidays import HolidayBase, __version__ as python_holidays_version import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_COUNTRY, CONF_NAME +from homeassistant.const import CONF_NAME from homeassistant.core import ( CALLBACK_TYPE, HomeAssistant, @@ -24,19 +24,9 @@ from homeassistant.helpers.entity_platform import ( async_get_current_platform, ) from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.util import dt as dt_util, slugify +from homeassistant.util import dt as dt_util -from .const import ( - ALLOWED_DAYS, - CONF_ADD_HOLIDAYS, - CONF_EXCLUDES, - CONF_OFFSET, - CONF_REMOVE_HOLIDAYS, - CONF_WORKDAYS, - DOMAIN, - LOGGER, -) +from .const import ALLOWED_DAYS, CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS, DOMAIN if TYPE_CHECKING: from . import WorkdayConfigEntry @@ -45,115 +35,17 @@ SERVICE_CHECK_DATE: Final = "check_date" CHECK_DATE: Final = "check_date" -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: date = d1 + timedelta(days=i) - calc_holidays.append(day.strftime("%Y-%m-%d")) - continue - calc_holidays.append(add_date) - return calc_holidays - - async def async_setup_entry( hass: HomeAssistant, entry: WorkdayConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Workday sensor.""" - 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]) excludes: list[str] = entry.options[CONF_EXCLUDES] sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] - obj_holidays = entry.runtime_data - calc_add_holidays: list[str] = validate_dates(add_holidays) - calc_remove_holidays: list[str] = validate_dates(remove_holidays) - next_year = dt_util.now().year + 1 - - # Add custom holidays - try: - 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 calc_remove_holidays: - try: - # is this formatted as a date? - if dt_util.parse_date(remove_holiday): - # remove holiday by date - removed = obj_holidays.pop(remove_holiday) - LOGGER.debug("Removed %s", remove_holiday) - else: - # remove holiday by name - LOGGER.debug("Treating '%s' as named holiday", remove_holiday) - removed = obj_holidays.pop_named(remove_holiday) - for holiday in removed: - LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) - except KeyError as unmatched: - LOGGER.warning("No holiday found matching %s", unmatched) - if _date := dt_util.parse_date(remove_holiday): - if _date.year <= next_year: - # Only check and raise issues for current and next year - async_create_issue( - hass, - DOMAIN, - f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_date_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_named_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - - LOGGER.debug("Found the following holidays for your configuration:") - for holiday_date, name in sorted(obj_holidays.items()): - # Make explicit str variable to avoid "Incompatible types in assignment" - _holiday_string = holiday_date.strftime("%Y-%m-%d") - LOGGER.debug("%s %s", _holiday_string, name) platform = async_get_current_platform() platform.async_register_entity_service( diff --git a/homeassistant/components/workday/util.py b/homeassistant/components/workday/util.py new file mode 100644 index 00000000000..aa1147a5b09 --- /dev/null +++ b/homeassistant/components/workday/util.py @@ -0,0 +1,27 @@ +"""Helpers functions for the Workday component.""" + +from datetime import date, timedelta + +from homeassistant.util import dt as dt_util + +from .const import LOGGER + + +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: date = d1 + timedelta(days=i) + calc_holidays.append(day.strftime("%Y-%m-%d")) + continue + calc_holidays.append(add_date) + return calc_holidays