diff --git a/README.md b/README.md index 1cd0566..e3d5bfc 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ Entity_id change is not possible using the YAML configuration. Changing other pa | `last_month` | Yes | Month three letter abbreviation.
**Default**: `"dec"` | `exclude_dates` | Yes | List of dates with no collection (using international date format `'yyyy-mm-dd'`. | `include_dates` | Yes | List of extra collection (using international date format `'yyyy-mm-dd'`. +| `move_country_holidays` | Yes | A country code (see [holidays](https://github.com/dr-prodigy/python-holidays) for the list of valid country codes).
Automatically move garbage collection on public holidays to the following day.
*Example:* `US` + #### PARAMETERS FOR COLLECTION EVERY-N-WEEKS |Attribute |Optional|Description diff --git a/custom_components/garbage_collection/.translations/cs.json b/custom_components/garbage_collection/.translations/cs.json index cb429fe..067bb36 100644 --- a/custom_components/garbage_collection/.translations/cs.json +++ b/custom_components/garbage_collection/.translations/cs.json @@ -57,7 +57,8 @@ "week_order_number_4": "Čtvrtý týden v měsíci", "week_order_number_5": "Pátý týden v měsíci", "include_dates": "Přidané datumy (volitelné)", - "exclude_dates": "Zakázané datumy (volitelné)" + "exclude_dates": "Zakázané datumy (volitelné)", + "move_country_holidays": "Přesunout svátky na další den (volitelné)" } } }, @@ -133,7 +134,8 @@ "week_order_number_4": "Čtvrtý týden v měsíci", "week_order_number_5": "Pátý týden v měsíci", "include_dates": "Přidané datumy (volitelné)", - "exclude_dates": "Zakázané datumy (volitelné)" + "exclude_dates": "Zakázané datumy (volitelné)", + "move_country_holidays": "Pčesunout svátky na další den (volitelné)" } } }, diff --git a/custom_components/garbage_collection/.translations/en.json b/custom_components/garbage_collection/.translations/en.json index c73eb73..dedd642 100644 --- a/custom_components/garbage_collection/.translations/en.json +++ b/custom_components/garbage_collection/.translations/en.json @@ -57,7 +57,8 @@ "week_order_number_4": "4th week of month", "week_order_number_5": "5th week of month", "include_dates": "Include dates (optional)", - "exclude_dates": "Exclude dates (optional)" + "exclude_dates": "Exclude dates (optional)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, @@ -132,7 +133,8 @@ "week_order_number_4": "4th week of month", "week_order_number_5": "5th week of month", "include_dates": "Include dates (optional)", - "exclude_dates": "Exclude dates (optional)" + "exclude_dates": "Exclude dates (optional)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, diff --git a/custom_components/garbage_collection/.translations/fr.json b/custom_components/garbage_collection/.translations/fr.json index c3ae28b..b71784b 100644 --- a/custom_components/garbage_collection/.translations/fr.json +++ b/custom_components/garbage_collection/.translations/fr.json @@ -57,7 +57,8 @@ "week_order_number_4": "4ème semaine du mois", "week_order_number_5": "5ème semaine du mois", "include_dates": "Inclusion de dates (optionel)", - "exclude_dates": "Exclusion de dates (optionel)" + "exclude_dates": "Exclusion de dates (optionel)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, @@ -132,8 +133,8 @@ "week_order_number_4": "4ème semaine du mois", "week_order_number_5": "5ème semaine du mois", "include_dates": "Inclusion de dates (optionel)", - "exclude_dates": "Exclusion de dates (optionel)" - + "exclude_dates": "Exclusion de dates (optionel)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, diff --git a/custom_components/garbage_collection/.translations/it.json b/custom_components/garbage_collection/.translations/it.json index fc1d744..716f800 100644 --- a/custom_components/garbage_collection/.translations/it.json +++ b/custom_components/garbage_collection/.translations/it.json @@ -57,7 +57,8 @@ "week_order_number_4": "4° settimana del mese", "week_order_number_5": "5° settimana del mese", "include_dates": "Includi date (opzionale)", - "exclude_dates": "Escludi date (opzionale)" + "exclude_dates": "Escludi date (opzionale)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, @@ -132,7 +133,8 @@ "week_order_number_4": "4° settimana del mese", "week_order_number_5": "5° settimana del mese", "include_dates": "Includi date (opzionale)", - "exclude_dates": "Escludi date (opzionale)" + "exclude_dates": "Escludi date (opzionale)", + "move_country_holidays": "Move holidays to next day (optional)" } } }, diff --git a/custom_components/garbage_collection/config_flow.py b/custom_components/garbage_collection/config_flow.py index a65d8e3..430888e 100644 --- a/custom_components/garbage_collection/config_flow.py +++ b/custom_components/garbage_collection/config_flow.py @@ -17,6 +17,7 @@ MONTHLY_FREQUENCY, ANNUAL_FREQUENCY, GROUP_FREQUENCY, + COUNTRY_CODES, DEFAULT_FIRST_MONTH, DEFAULT_LAST_MONTH, DEFAULT_FREQUENCY, @@ -44,6 +45,7 @@ CONF_DATE, CONF_EXCLUDE_DATES, CONF_INCLUDE_DATES, + CONF_MOVE_COUNTRY_HOLIDAYS, CONF_PERIOD, CONF_FIRST_WEEK, CONF_SENSORS, @@ -286,6 +288,9 @@ async def async_step_final( final_info[CONF_EXCLUDE_DATES] = string_to_list( user_input[CONF_EXCLUDE_DATES] ) + final_info[CONF_MOVE_COUNTRY_HOLIDAYS] = user_input[ + CONF_MOVE_COUNTRY_HOLIDAYS + ] if not is_dates(final_info[CONF_INCLUDE_DATES]) or not is_dates( final_info[CONF_EXCLUDE_DATES] ): @@ -311,6 +316,7 @@ async def _show_final_form(self, user_input): last_month = DEFAULT_LAST_MONTH include_dates = "" exclude_dates = "" + include_country_holidays = "" period = 1 first_week = 1 if user_input is not None: @@ -326,6 +332,8 @@ async def _show_final_form(self, user_input): include_dates = user_input[CONF_INCLUDE_DATES] if CONF_EXCLUDE_DATES in user_input: exclude_dates = user_input[CONF_EXCLUDE_DATES] + if CONF_MOVE_COUNTRY_HOLIDAYS in user_input: + include_country_holidays = user_input[CONF_MOVE_COUNTRY_HOLIDAYS] data_schema = OrderedDict() data_schema[vol.Optional(CONF_FIRST_MONTH, default=first_month)] = vol.In( MONTH_OPTIONS @@ -366,6 +374,9 @@ async def _show_final_form(self, user_input): ] = bool data_schema[vol.Optional(CONF_INCLUDE_DATES, default=include_dates)] = str data_schema[vol.Optional(CONF_EXCLUDE_DATES, default=exclude_dates)] = str + data_schema[ + vol.Optional(CONF_MOVE_COUNTRY_HOLIDAYS, default=include_country_holidays) + ] = vol.In(COUNTRY_CODES) return self.async_show_form( step_id="final", data_schema=vol.Schema(data_schema), errors=self._errors ) @@ -667,6 +678,9 @@ async def async_step_final( final_info[CONF_EXCLUDE_DATES] ): self._errors["base"] = "date" + final_info[CONF_MOVE_COUNTRY_HOLIDAYS] = user_input[ + CONF_MOVE_COUNTRY_HOLIDAYS + ] if self._data[CONF_FREQUENCY] in WEEKLY_FREQUENCY_X: final_info[CONF_PERIOD] = user_input[CONF_PERIOD] final_info[CONF_FIRST_WEEK] = user_input[CONF_FIRST_WEEK] @@ -737,6 +751,12 @@ async def _show_final_form(self, user_input): default=",".join(self.config_entry.options.get(CONF_EXCLUDE_DATES)), ) ] = str + data_schema[ + vol.Optional( + CONF_MOVE_COUNTRY_HOLIDAYS, + default=self.config_entry.options.get(CONF_MOVE_COUNTRY_HOLIDAYS), + ) + ] = vol.In(COUNTRY_CODES) return self.async_show_form( step_id="final", data_schema=vol.Schema(data_schema), errors=self._errors ) diff --git a/custom_components/garbage_collection/const.py b/custom_components/garbage_collection/const.py index de96295..e58d78a 100644 --- a/custom_components/garbage_collection/const.py +++ b/custom_components/garbage_collection/const.py @@ -35,6 +35,7 @@ CONF_DATE = "date" CONF_EXCLUDE_DATES = "exclude_dates" CONF_INCLUDE_DATES = "include_dates" +CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays" CONF_PERIOD = "period" CONF_FIRST_WEEK = "first_week" CONF_SENSORS = "sensors" @@ -87,6 +88,57 @@ "dec", ] +COUNTRY_CODES = [ + "AR", + "AT", + "AU", + "AW", + "BE", + "BG", + "BR", + "BY", + "CA", + "CH", + "CO", + "CZ", + "DE", + "DK", + "DO", + "ECB", + "EE", + "ES", + "FI", + "FRA", + "HR", + "HU", + "IE", + "IND", + "IS", + "IT", + "JP", + "KE", + "LT", + "LU", + "MX", + "NG", + "NI", + "NL", + "NO", + "NZ", + "PE", + "PL", + "PT", + "PTE", + "RU", + "SE", + "SI", + "SK", + "UA", + "UK", + "US", + "ZA", +] + def date_text(value): if value is None or value == "": @@ -137,6 +189,7 @@ def month_day_text(value): vol.Optional(CONF_EXCLUDE_DATES, default=[]): vol.All( cv.ensure_list, [date_text] ), + vol.Optional(CONF_MOVE_COUNTRY_HOLIDAYS): vol.In(COUNTRY_CODES), vol.Optional(CONF_ICON_NORMAL, default=DEFAULT_ICON_NORMAL): cv.icon, vol.Optional(CONF_ICON_TODAY, default=DEFAULT_ICON_TODAY): cv.icon, vol.Optional(CONF_ICON_TOMORROW, default=DEFAULT_ICON_TOMORROW): cv.icon, @@ -148,11 +201,8 @@ def month_day_text(value): CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]) - } + {vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA])} ) }, extra=vol.ALLOW_EXTRA, diff --git a/custom_components/garbage_collection/manifest.json b/custom_components/garbage_collection/manifest.json index 92ab482..7354161 100644 --- a/custom_components/garbage_collection/manifest.json +++ b/custom_components/garbage_collection/manifest.json @@ -10,6 +10,7 @@ "requirements": [ "datetime", "integrationhelper", + "holidays", "typing", "uuid", "voluptuous" diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 4c8be32..57829b6 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for garbage_collection.""" from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util +import holidays import logging import locale from datetime import datetime, date, timedelta @@ -39,6 +40,7 @@ CONF_DATE, CONF_EXCLUDE_DATES, CONF_INCLUDE_DATES, + CONF_MOVE_COUNTRY_HOLIDAYS, CONF_PERIOD, CONF_FIRST_WEEK, CONF_SENSORS, @@ -129,6 +131,18 @@ def __init__(self, hass, config): ) self.__include_dates = to_dates(config.get(CONF_INCLUDE_DATES, [])) self.__exclude_dates = to_dates(config.get(CONF_EXCLUDE_DATES, [])) + country_holidays = config.get(CONF_MOVE_COUNTRY_HOLIDAYS) + self.__holidays = [] + if country_holidays is not None and country_holidays != "": + this_year = dt_util.now().date().year + years = [this_year, this_year + 1] + try: + for date, name in holidays.CountryHoliday( + country_holidays, years=years + ).items(): + self.__holidays.append(date) + except KeyError: + _LOGGER.error("Invalid country code (%s)", country_holidays) self.__period = config.get(CONF_PERIOD) self.__first_week = config.get(CONF_FIRST_WEEK) self.__next_date = None @@ -291,28 +305,31 @@ def find_candidate_date(self, day1: date) -> date: _LOGGER.debug(f"({self.__name}) Unknown frequency {self.__frequency}") return None + def __insert_include_date(self, day1: date, next_date: date) -> date: + include_dates = list(filter(lambda date: date >= day1, self.__include_dates)) + if len(include_dates) > 0 and include_dates[0] < next_date: + return include_dates[0] + else: + return next_date + + def __skip_holiday(self, day: date) -> date: + return day + timedelta(days=1) + def get_next_date(self, day1: date) -> date: - """Find the next date starting from day1. - Looks at include and exclude days""" + """Find the next date starting from day1.""" first_day = day1 i = 0 - while True: + while i < 365: next_date = self.find_candidate_date(first_day) - include_dates = list( - filter(lambda date: date >= day1, self.__include_dates) - ) - if len(include_dates) > 0 and include_dates[0] < next_date: - next_date = include_dates[0] + while next_date in self.__holidays: + next_date = self.__skip_holiday(next_date) + next_date = self.__insert_include_date(first_day, next_date) if next_date not in self.__exclude_dates: - break - else: - first_day = next_date + timedelta(days=1) + return next_date + first_day = next_date + timedelta(days=1) i += 1 - if i > 365: - _LOGGER.error("(%s) Cannot find any suitable date", self.__name) - next_date = None - break - return next_date + _LOGGER.error("(%s) Cannot find any suitable date", self.__name) + return None async def async_update(self) -> None: """Get the latest data and updates the states."""