Décalage jours fériés mobiles + config helpers
This commit is contained in:
@@ -95,9 +95,9 @@ async def general_config_schema(
|
|||||||
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
|
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
|
||||||
): bool,
|
): bool,
|
||||||
optional(
|
optional(
|
||||||
const.CONF_HOLIDAY_COUNTRY,
|
const.CONF_HOLIDAY_DATES,
|
||||||
handler.options,
|
handler.options,
|
||||||
const.DEFAULT_HOLIDAY_COUNTRY,
|
const.DEFAULT_HOLIDAY_DATES,
|
||||||
): selector.TextSelector(),
|
): selector.TextSelector(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -135,9 +135,9 @@ async def general_options_schema(
|
|||||||
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
|
const.DEFAULT_HOLIDAY_IN_WEEK_MOVE,
|
||||||
): bool,
|
): bool,
|
||||||
optional(
|
optional(
|
||||||
const.CONF_HOLIDAY_COUNTRY,
|
const.CONF_HOLIDAY_DATES,
|
||||||
handler.options,
|
handler.options,
|
||||||
const.DEFAULT_HOLIDAY_COUNTRY,
|
const.DEFAULT_HOLIDAY_DATES,
|
||||||
): selector.TextSelector(),
|
): selector.TextSelector(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ CONF_SENSORS = "sensors"
|
|||||||
CONF_VERBOSE_FORMAT = "verbose_format"
|
CONF_VERBOSE_FORMAT = "verbose_format"
|
||||||
CONF_DATE_FORMAT = "date_format"
|
CONF_DATE_FORMAT = "date_format"
|
||||||
CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays"
|
CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays"
|
||||||
CONF_HOLIDAY_COUNTRY = "holiday_country"
|
CONF_HOLIDAY_DATES = "holiday_dates"
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
DEFAULT_NAME = DOMAIN
|
DEFAULT_NAME = DOMAIN
|
||||||
@@ -55,7 +55,7 @@ DEFAULT_PERIOD = 1
|
|||||||
DEFAULT_FIRST_WEEK = 1
|
DEFAULT_FIRST_WEEK = 1
|
||||||
DEFAULT_VERBOSE_STATE = False
|
DEFAULT_VERBOSE_STATE = False
|
||||||
DEFAULT_HOLIDAY_IN_WEEK_MOVE = 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_DATE_FORMAT = "%d-%b-%Y"
|
||||||
DEFAULT_VERBOSE_FORMAT = "on {date}, in {days} days"
|
DEFAULT_VERBOSE_FORMAT = "on {date}, in {days} days"
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"issue_tracker": "https://github.com/bruxy70/Garbage-Collection/issues",
|
"issue_tracker": "https://github.com/bruxy70/Garbage-Collection/issues",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"python-dateutil>=2.8.2",
|
"python-dateutil>=2.8.2"
|
||||||
"holidays>=0.40"
|
|
||||||
],
|
],
|
||||||
"version": "3.21.1"
|
"version": "3.21.3"
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,8 @@ import logging
|
|||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
from typing import Any, Dict, Generator
|
from typing import Any, Dict, Generator
|
||||||
|
|
||||||
|
from dateutil.easter import easter
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
import holidays
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
@@ -71,8 +71,8 @@ class GarbageCollection(RestoreEntity):
|
|||||||
"_days",
|
"_days",
|
||||||
"_first_month",
|
"_first_month",
|
||||||
"_hidden",
|
"_hidden",
|
||||||
"_holiday_country",
|
"_holiday_dates",
|
||||||
"_holidays_calendar",
|
"_move_holidays",
|
||||||
"_icon_normal",
|
"_icon_normal",
|
||||||
"_icon_today",
|
"_icon_today",
|
||||||
"_icon_tomorrow",
|
"_icon_tomorrow",
|
||||||
@@ -126,23 +126,25 @@ class GarbageCollection(RestoreEntity):
|
|||||||
self._verbose_format = config.get(
|
self._verbose_format = config.get(
|
||||||
const.CONF_VERBOSE_FORMAT, const.DEFAULT_VERBOSE_FORMAT
|
const.CONF_VERBOSE_FORMAT, const.DEFAULT_VERBOSE_FORMAT
|
||||||
)
|
)
|
||||||
self._holiday_country = config.get(
|
self._holiday_dates: set[tuple[int, int]] = set()
|
||||||
const.CONF_HOLIDAY_COUNTRY, const.DEFAULT_HOLIDAY_COUNTRY
|
self._move_holidays = bool(config.get(const.CONF_MOVE_COUNTRY_HOLIDAYS, False))
|
||||||
)
|
if self._move_holidays:
|
||||||
if config.get(const.CONF_MOVE_COUNTRY_HOLIDAYS, False):
|
raw_dates = config.get(
|
||||||
try:
|
const.CONF_HOLIDAY_DATES, const.DEFAULT_HOLIDAY_DATES
|
||||||
self._holidays_calendar = holidays.country_holidays(
|
)
|
||||||
self._holiday_country
|
for chunk in raw_dates.split(","):
|
||||||
)
|
chunk = chunk.strip()
|
||||||
except NotImplementedError:
|
if not chunk:
|
||||||
_LOGGER.error(
|
continue
|
||||||
"(%s) Unknown country code for holidays: %s",
|
try:
|
||||||
self._attr_name,
|
month_str, day_str = chunk.split("-")
|
||||||
self._holiday_country,
|
self._holiday_dates.add((int(month_str), int(day_str)))
|
||||||
)
|
except ValueError:
|
||||||
self._holidays_calendar = None
|
_LOGGER.error(
|
||||||
else:
|
"(%s) Invalid holiday date '%s', expected format MM-DD",
|
||||||
self._holidays_calendar = None
|
self._attr_name,
|
||||||
|
chunk,
|
||||||
|
)
|
||||||
self._collection_dates: list[date] = []
|
self._collection_dates: list[date] = []
|
||||||
self._next_date: date | None = None
|
self._next_date: date | None = None
|
||||||
self._last_updated: datetime | None = None
|
self._last_updated: datetime | None = None
|
||||||
@@ -378,31 +380,40 @@ class GarbageCollection(RestoreEntity):
|
|||||||
yield next_date
|
yield next_date
|
||||||
first_date = next_date + relativedelta(days=1) # look from the next day
|
first_date = next_date + relativedelta(days=1) # look from the next day
|
||||||
|
|
||||||
def _shift_for_holiday(self, collection_date: date) -> date:
|
def _is_exception_date(self, day: date) -> bool:
|
||||||
"""Move the date forward if a public holiday falls on it or earlier that week.
|
"""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
|
Fixed exceptions come from the configured MM-DD list (e.g. Christmas,
|
||||||
that range is a holiday, move forward by one day. Then keep moving forward,
|
New Year, Labour Day for collectors that only shift on those).
|
||||||
one day at a time, while the new candidate day is itself a holiday.
|
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
|
return collection_date
|
||||||
monday = collection_date - timedelta(days=collection_date.weekday())
|
shifted = collection_date
|
||||||
week_has_holiday = any(
|
while self._is_exception_date(shifted):
|
||||||
(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 = shifted + timedelta(days=1)
|
shifted = shifted + timedelta(days=1)
|
||||||
_LOGGER.debug(
|
if shifted != collection_date:
|
||||||
"(%s) %s shifted to %s because of a public holiday",
|
_LOGGER.debug(
|
||||||
self._attr_name,
|
"(%s) %s shifted to %s because of a configured exception date",
|
||||||
collection_date,
|
self._attr_name,
|
||||||
shifted,
|
collection_date,
|
||||||
)
|
shifted,
|
||||||
|
)
|
||||||
return shifted
|
return shifted
|
||||||
|
|
||||||
async def _async_load_collection_dates(self) -> None:
|
async def _async_load_collection_dates(self) -> None:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"expire_after": "Expire after (HH:MM) - optional",
|
"expire_after": "Expire after (HH:MM) - optional",
|
||||||
"verbose_state": "Verbose state (text, instead of number)",
|
"verbose_state": "Verbose state (text, instead of number)",
|
||||||
"move_country_holidays": "Shift collection if a public holiday falls in the week",
|
"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": {
|
"detail": {
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"expire_after": "Expire after (HH:MM) - optional",
|
"expire_after": "Expire after (HH:MM) - optional",
|
||||||
"verbose_state": "Verbose state (text, instead of number)",
|
"verbose_state": "Verbose state (text, instead of number)",
|
||||||
"move_country_holidays": "Shift collection if a public holiday falls in the week",
|
"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": {
|
"detail": {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"expire_after": "Expire après (HH:MM)",
|
"expire_after": "Expire après (HH:MM)",
|
||||||
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
|
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
|
||||||
"move_country_holidays": "Décaler la collecte si jour férié dans la semaine",
|
"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": {
|
"detail": {
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"expire_after": "Expire après (HH:MM)",
|
"expire_after": "Expire après (HH:MM)",
|
||||||
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
|
"verbose_state": "Etat verbeux (texte, au lieu du chiffre)",
|
||||||
"move_country_holidays": "Décaler la collecte si jour férié dans la semaine",
|
"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": {
|
"detail": {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user