Décalage jours fériés mobiles + config helpers

This commit is contained in:
2026-06-17 12:45:35 +02:00
parent 6da3332a73
commit 35d6cf71dd
7 changed files with 64 additions and 54 deletions
@@ -95,9 +95,9 @@ async def general_config_schema(
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
): bool,
optional(
const.CONF_HOLIDAY_COUNTRY,
const.CONF_HOLIDAY_DATES,
handler.options,
const.DEFAULT_HOLIDAY_COUNTRY,
const.DEFAULT_HOLIDAY_DATES,
): selector.TextSelector(),
}
)
@@ -135,9 +135,9 @@ async def general_options_schema(
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
): bool,
optional(
const.CONF_HOLIDAY_COUNTRY,
const.CONF_HOLIDAY_DATES,
handler.options,
const.DEFAULT_HOLIDAY_COUNTRY,
const.DEFAULT_HOLIDAY_DATES,
): selector.TextSelector(),
}
)
@@ -44,7 +44,7 @@ CONF_SENSORS = "sensors"
CONF_VERBOSE_FORMAT = "verbose_format"
CONF_DATE_FORMAT = "date_format"
CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays"
CONF_HOLIDAY_COUNTRY = "holiday_country"
CONF_HOLIDAY_DATES = "holiday_dates"
# Defaults
DEFAULT_NAME = DOMAIN
@@ -55,7 +55,7 @@ DEFAULT_PERIOD = 1
DEFAULT_FIRST_WEEK = 1
DEFAULT_VERBOSE_STATE = False
DEFAULT_HOLIDAY_IN_WEEK_MOVE = False
DEFAULT_HOLIDAY_COUNTRY = "FR"
DEFAULT_HOLIDAY_DATES = "01-01,05-01,12-25"
DEFAULT_DATE_FORMAT = "%d-%b-%Y"
DEFAULT_VERBOSE_FORMAT = "on {date}, in {days} days"
@@ -11,8 +11,7 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/bruxy70/Garbage-Collection/issues",
"requirements": [
"python-dateutil>=2.8.2",
"holidays>=0.40"
"python-dateutil>=2.8.2"
],
"version": "3.21.1"
"version": "3.21.3"
}
+42 -31
View File
@@ -5,8 +5,8 @@ import logging
from datetime import date, datetime, time, timedelta
from typing import Any, Dict, Generator
from dateutil.easter import easter
from dateutil.relativedelta import relativedelta
import holidays
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -71,8 +71,8 @@ class GarbageCollection(RestoreEntity):
"_days",
"_first_month",
"_hidden",
"_holiday_country",
"_holidays_calendar",
"_holiday_dates",
"_move_holidays",
"_icon_normal",
"_icon_today",
"_icon_tomorrow",
@@ -126,23 +126,25 @@ class GarbageCollection(RestoreEntity):
self._verbose_format = config.get(
const.CONF_VERBOSE_FORMAT, const.DEFAULT_VERBOSE_FORMAT
)
self._holiday_country = config.get(
const.CONF_HOLIDAY_COUNTRY, const.DEFAULT_HOLIDAY_COUNTRY
self._holiday_dates: set[tuple[int, int]] = set()
self._move_holidays = bool(config.get(const.CONF_MOVE_COUNTRY_HOLIDAYS, False))
if self._move_holidays:
raw_dates = config.get(
const.CONF_HOLIDAY_DATES, const.DEFAULT_HOLIDAY_DATES
)
if config.get(const.CONF_MOVE_COUNTRY_HOLIDAYS, False):
for chunk in raw_dates.split(","):
chunk = chunk.strip()
if not chunk:
continue
try:
self._holidays_calendar = holidays.country_holidays(
self._holiday_country
)
except NotImplementedError:
month_str, day_str = chunk.split("-")
self._holiday_dates.add((int(month_str), int(day_str)))
except ValueError:
_LOGGER.error(
"(%s) Unknown country code for holidays: %s",
"(%s) Invalid holiday date '%s', expected format MM-DD",
self._attr_name,
self._holiday_country,
chunk,
)
self._holidays_calendar = None
else:
self._holidays_calendar = None
self._collection_dates: list[date] = []
self._next_date: date | None = None
self._last_updated: datetime | None = None
@@ -378,27 +380,36 @@ class GarbageCollection(RestoreEntity):
yield next_date
first_date = next_date + relativedelta(days=1) # look from the next day
def _shift_for_holiday(self, collection_date: date) -> date:
"""Move the date forward if a public holiday falls on it or earlier that week.
def _is_exception_date(self, day: date) -> bool:
"""Check if a date matches a configured fixed exception, or a movable one.
First check Monday..collection_date (inclusive) for a holiday: if any day in
that range is a holiday, move forward by one day. Then keep moving forward,
one day at a time, while the new candidate day is itself a holiday.
Fixed exceptions come from the configured MM-DD list (e.g. Christmas,
New Year, Labour Day for collectors that only shift on those).
Movable exceptions are always checked too: Easter Monday and Whit
Monday (Pentecost Monday) are the only French public holidays that
ever fall on a Monday, which only matters for Monday-based schedules
and is harmless to check for any other day.
"""
if self._holidays_calendar is None:
if (day.month, day.day) in self._holiday_dates:
return True
easter_sunday = easter(day.year)
easter_monday = easter_sunday + timedelta(days=1)
pentecost_monday = easter_sunday + timedelta(days=50)
return day in (easter_monday, pentecost_monday)
def _shift_for_holiday(self, collection_date: date) -> date:
"""Move the date forward by one day for each matching exception date.
Cascades forward if the new day also matches an exception date.
"""
if not self._move_holidays:
return collection_date
monday = collection_date - timedelta(days=collection_date.weekday())
week_has_holiday = any(
(monday + timedelta(days=i)) in self._holidays_calendar
for i in range(collection_date.weekday() + 1)
)
if not week_has_holiday:
return collection_date
shifted = collection_date + timedelta(days=1)
while shifted in self._holidays_calendar:
shifted = collection_date
while self._is_exception_date(shifted):
shifted = shifted + timedelta(days=1)
if shifted != collection_date:
_LOGGER.debug(
"(%s) %s shifted to %s because of a public holiday",
"(%s) %s shifted to %s because of a configured exception date",
self._attr_name,
collection_date,
shifted,
@@ -15,7 +15,7 @@
"expire_after": "Expire after (HH:MM) - optional",
"verbose_state": "Verbose state (text, instead of number)",
"move_country_holidays": "Shift collection if a public holiday falls in the week",
"holiday_country": "Country code for public holidays (e.g. FR)"
"holiday_dates": "Dates that shift collection by one day (format MM-DD, comma separated)"
}
},
"detail": {
@@ -69,7 +69,7 @@
"expire_after": "Expire after (HH:MM) - optional",
"verbose_state": "Verbose state (text, instead of number)",
"move_country_holidays": "Shift collection if a public holiday falls in the week",
"holiday_country": "Country code for public holidays (e.g. FR)"
"holiday_dates": "Dates that shift collection by one day (format MM-DD, comma separated)"
}
},
"detail": {
@@ -16,7 +16,7 @@
"expire_after": "Expire après (HH:MM)",
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
"move_country_holidays": "Décaler la collecte si jour férié dans la semaine",
"holiday_country": "Code pays pour les jours fériés (ex: FR)"
"holiday_dates": "Dates qui décalent la collecte d'un jour (format MM-JJ, séparées par des virgules)"
}
},
"detail": {
@@ -71,7 +71,7 @@
"expire_after": "Expire après (HH:MM)",
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
"move_country_holidays": "Décaler la collecte si jour férié dans la semaine",
"holiday_country": "Code pays pour les jours fériés (ex: FR)"
"holiday_dates": "Dates qui décalent la collecte d'un jour (format MM-JJ, séparées par des virgules)"
}
},
"detail": {
Binary file not shown.