Calculate holidays locally and retire remote countdowns

Preserve existing countdown and calendar entity IDs while computing floating
holidays from weekday, Gregorian Easter, and Hebrew-calendar rules. Remove
obsolete example credentials and expiring date tables, correct Memorial Day
and Mardi Gras, and cover the Hanukkah lighting window.

Align holiday speech with today/tomorrow countdown values and extend the
existing validation gate with calendar, rollover, lighting, and speech checks.
codex/local-holiday-calendars
Carlo Costanzo 4 weeks ago
parent e8a16c9681
commit 49c3cfa1e9

@ -5,13 +5,16 @@ from __future__ import annotations
import argparse
import calendar
import importlib.util
import json
import sys
from dataclasses import dataclass
from datetime import date, timedelta
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Callable, Iterable
from jinja2 import Environment, StrictUndefined
from validate_dashboards import ValidationError as YamlValidationError
from validate_dashboards import parse_yaml
@ -93,12 +96,27 @@ def last_weekday(year: int, month: int, weekday: int) -> date:
DYNAMIC_RULES = (
DynamicRule("MLK Day", "holidays", lambda year: nth_weekday(year, 1, 0, 3)),
DynamicRule("Presidents Day", "holidays", lambda year: nth_weekday(year, 2, 0, 3)),
DynamicRule("Mardi Gras", "holidays", lambda year: gregorian_easter(year) - timedelta(days=47)),
DynamicRule("Ash Wednesday", "holidays", lambda year: gregorian_easter(year) - timedelta(days=46)),
DynamicRule("Palm Sunday", "holidays", lambda year: gregorian_easter(year) - timedelta(days=7)),
DynamicRule("Good Friday", "holidays", lambda year: gregorian_easter(year) - timedelta(days=2)),
DynamicRule("Easter Sunday", "holidays", gregorian_easter),
DynamicRule("Mothers Day", "holidays", lambda year: nth_weekday(year, 5, 6, 2)),
DynamicRule("Fathers Day", "holidays", lambda year: nth_weekday(year, 6, 6, 3)),
DynamicRule("Thanksgiving Day", "holidays", lambda year: nth_weekday(year, 11, 3, 4)),
DynamicRule("Memorial Day", "holidays", lambda year: last_weekday(year, 5, 0)),
DynamicRule("Labor Day", "holidays", lambda year: nth_weekday(year, 9, 0, 1)),
DynamicRule("Columbus Day", "holidays", lambda year: nth_weekday(year, 10, 0, 2)),
DynamicRule("Martin Luther King Jr. Day", "flag_days", lambda year: nth_weekday(year, 1, 0, 3)),
DynamicRule("Washington's Birthday (Presidents Day)", "flag_days", lambda year: nth_weekday(year, 2, 0, 3)),
DynamicRule("Armed Forces Day", "flag_days", lambda year: nth_weekday(year, 5, 5, 3)),
DynamicRule("Memorial Day", "flag_days", lambda year: last_weekday(year, 5, 0)),
DynamicRule("Labor Day", "flag_days", lambda year: nth_weekday(year, 9, 0, 1)),
DynamicRule("National POW/MIA Recognition Day", "flag_days", lambda year: nth_weekday(year, 9, 4, 3)),
DynamicRule("Columbus Day", "flag_days", lambda year: nth_weekday(year, 10, 0, 2)),
DynamicRule("Election Day", "flag_days", lambda year: nth_weekday(year, 11, 0, 1) + timedelta(days=1)),
)
@ -178,7 +196,7 @@ def validate_dynamic_rule(
actual = {
parse_calendar_date(key, rule.source)
for key, value in dynamic_map.items()
if value == rule.name and start <= parse_calendar_date(key, rule.source) < end
if rule.name in value.split("; ") and start <= parse_calendar_date(key, rule.source) < end
}
if actual != expected:
missing = sorted(expected - actual)
@ -239,8 +257,8 @@ def validate_routing_contract(package_path: Path, script_path: Path) -> None:
raise CoverageError(f"cannot read holiday routing configuration: {exc}") from exc
required_package_fragments = (
"resource: http://localhost:8123/local/json_data/holidays.json",
"resource: http://localhost:8123/local/json_data/flag_days.json",
"unique_id: holiday_calendar",
"unique_id: flag_calendar",
"unique_id: holiday_lighting_mode",
"unique_id: holiday_lighting_scene",
"scene.month_{{ mode }}_colors",
@ -277,10 +295,23 @@ def validate_coverage(root: Path, start: date, months: int) -> dict[str, tuple[d
holidays = _require_mapping(read_json(holiday_path).get("MAJOR_US"), "MAJOR_US")
flags = _require_mapping(read_json(flag_path).get("Flag_Days_US"), "Flag_Days_US")
holiday_static = _require_mapping(holidays.get("static"), "MAJOR_US.static")
holiday_dynamic = _require_mapping(holidays.get("dynamic"), "MAJOR_US.dynamic")
flag_dynamic = _require_mapping(flags.get("dynamic"), "Flag_Days_US.dynamic")
validate_dynamic_map(holiday_dynamic, "holidays")
validate_dynamic_map(flag_dynamic, "flag_days")
for source, data in (("holidays", holidays), ("flag_days", flags)):
additions = _require_mapping(data.get("dynamic"), f"{source}.dynamic")
validate_dynamic_map(additions, source)
ruled_names = {rule.name for rule in DYNAMIC_RULES if rule.source == source}
ruled_names |= {"Chanukkah", "Inauguration Day"}
for name in additions.values():
if any(event in ruled_names for event in name.split("; ")):
raise CoverageError(f"{source}: {name} belongs in perpetual rules, not dated additions")
calculator = load_calculator(root)
holiday_dynamic: dict[str, object] = {}
flag_dynamic: dict[str, object] = {}
for year in range(start.year, end.year + 1):
anchor = date(year, 7, 1)
data_dir = holiday_path.parent
holiday_dynamic.update(calculator.calendar_data("holiday", anchor, data_dir)["MAJOR_US"]["dynamic"])
flag_dynamic.update(calculator.calendar_data("flag", anchor, data_dir)["Flag_Days_US"]["dynamic"])
for key, expected_name in STATIC_REQUIREMENTS.items():
actual_name = holiday_static.get(key)
@ -292,7 +323,9 @@ def validate_coverage(root: Path, start: date, months: int) -> dict[str, tuple[d
dates: dict[str, tuple[date, ...]] = {}
for rule in DYNAMIC_RULES:
source_map = holiday_dynamic if rule.source == "holidays" else flag_dynamic
dates[rule.name] = validate_dynamic_rule(rule, source_map, start, end)
dates[f"{rule.source}: {rule.name}"] = validate_dynamic_rule(rule, source_map, start, end)
validate_runtime_boundaries(root, calculator)
validate_scenes(scene_path)
validate_routing_contract(
@ -302,6 +335,123 @@ def validate_coverage(root: Path, start: date, months: int) -> dict[str, tuple[d
return dates
def load_calculator(root: Path):
path = root / "config" / "shell_scripts" / "holiday_countdowns.py"
spec = importlib.util.spec_from_file_location("holiday_countdowns", path)
if spec is None or spec.loader is None:
raise CoverageError(f"cannot load local calendar calculator: {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def validate_runtime_boundaries(root: Path, calculator) -> None:
"""Exercise real templates, including faults the old JSON-only gate missed."""
package = parse_yaml(root / "config" / "packages" / "holiday.yaml")
sensors = package["template"][0]["sensor"]
mode_template = next(sensor["state"] for sensor in sensors if sensor["unique_id"] == "holiday_lighting_mode")
env = Environment(undefined=StrictUndefined)
data_dir = root / "config" / "www" / "json_data"
def require(actual, expected, label):
if actual != expected:
raise CoverageError(f"{label}: expected {expected!r}, got {actual!r}")
# Independently published 25 Kislev dates, including a late December festival.
# https://www.hebcal.com/holidays/chanukah-2026
# https://www.hebcal.com/holidays/chanukah-2027
for first_day in (date(2026, 12, 5), date(2027, 12, 25)):
for offset in (-1, 0, 1):
today = first_day + timedelta(days=offset)
target = calculator.next_holiday("chanukkah", today)
if offset <= 0:
require(target, first_day, f"Chanukkah {today}")
elif target <= today:
raise CoverageError(f"Chanukkah failed annual rollover: {today}")
countdown_rules = {
"halloween": lambda year: date(year, 10, 31),
"christmas": lambda year: date(year, 12, 25),
"easter": gregorian_easter,
"mothers": lambda year: nth_weekday(year, 5, 6, 2),
"fathers": lambda year: nth_weekday(year, 6, 6, 3),
"memorial_day": lambda year: last_weekday(year, 5, 0),
"labor_day": lambda year: nth_weekday(year, 9, 0, 1),
"thanksgiving_day": lambda year: nth_weekday(year, 11, 3, 4),
}
for year in (2026, 2027, 2028, 2031, 2038, 2040):
for name, rule in countdown_rules.items():
event = rule(year)
for offset in (-1, 0, 1):
today = event + timedelta(days=offset)
expected = event if offset <= 0 else rule(year + 1)
require(calculator.next_holiday(name, today), expected, f"{name} rollover {today}")
cases = {
"2026-01-01": "new_years_day", "2026-01-19": "RWB",
"2026-02-09": "standard", "2026-02-10": "valentine", "2026-02-12": "RWB",
"2026-02-17": "mardi_gras", "2026-03-05": "standard",
"2026-03-14": "pi", "2026-03-15": "st_patty",
"2026-04-02": "easter", "2026-04-06": "standard", "2026-04-20": "standard",
"2026-05-04": "starwars", "2026-05-05": "cinco_de_mayo",
"2026-05-07": "mothers_day", "2026-05-10": "mothers_day", "2026-05-11": "standard",
"2026-05-23": "RWB", "2026-05-25": "RWB",
"2026-06-18": "fathers_day", "2026-06-21": "fathers_day",
"2026-09-05": "RWB", "2026-09-07": "RWB",
"2026-10-01": "halloween", "2026-10-27": "RWB", "2026-11-11": "RWB",
"2026-11-23": "thanksgiving", "2026-11-26": "thanksgiving",
"2026-11-27": "standard", "2026-11-30": "christmas", "2026-12-03": "christmas",
"2026-12-04": "hanukkah", "2026-12-05": "hanukkah", "2026-12-06": "hanukkah", "2026-12-07": "RWB",
"2026-12-08": "hanukkah", "2026-12-09": "hanukkah", "2026-12-10": "hanukkah",
"2026-12-11": "hanukkah", "2026-12-12": "christmas",
"2026-12-25": "christmas", "2026-12-26": "standard", "2026-12-31": "new_years_day",
"2027-02-09": "mardi_gras", "2028-02-29": "mardi_gras", "2029-02-13": "mardi_gras",
"2027-12-24": "hanukkah", "2027-12-25": "hanukkah", "2027-12-26": "hanukkah", "2027-12-27": "hanukkah",
"2027-12-28": "hanukkah", "2027-12-29": "hanukkah", "2027-12-30": "hanukkah", "2027-12-31": "hanukkah", "2028-01-01": "new_years_day",
"2038-04-22": "easter", "2040-05-28": "RWB",
}
for iso_date, expected in cases.items():
today = date.fromisoformat(iso_date)
holiday = calculator.calendar_data("holiday", today, data_dir)
flag = calculator.calendar_data("flag", today, data_dir)
states = {"sensor.holiday": holiday["state"], "sensor.flag": flag["state"]}
states.update({f"sensor.{name}_countdown": str((calculator.next_holiday(name, today) - today).days)
for name in [*countdown_rules, "chanukkah"]})
env.globals.update(
now=lambda: datetime.combine(today, datetime.min.time()),
strptime=lambda text, fmt, default=None: datetime.strptime(text, fmt),
states=lambda entity: states.get(entity, "unknown"),
is_state=lambda entity, value: states.get(entity) == value,
state_attr=lambda entity, attr: holiday.get(attr) if entity == "sensor.holiday" else flag.get(attr),
)
require(env.from_string(mode_template).render().strip(), expected, f"lighting {today}")
# On Jan 20, 2025 both events must survive the calendar's date-key collision.
collision = calculator.calendar_data("flag", date(2025, 1, 20), data_dir)
names = collision["Flag_Days_US"]["dynamic"]["01/20/2025"].split("; ")
require(set(names), {"Martin Luther King Jr. Day", "Inauguration Day"}, "shared flag date")
require(calculator.calendar_data("holiday", date(2027, 5, 31), data_dir)["state"], "Memorial Day", "Memorial name")
require(calculator.calendar_data("holiday", date(2027, 4, 19), data_dir)["state"], "", "false April Memorial Day")
require(calculator.calendar_data("flag", date(2033, 1, 20), data_dir)["state"], "True", "future inauguration")
speech = (root / "config" / "templates" / "speech" / "briefing.yaml").read_text(encoding="utf-8")
macro_start = speech.index("{%- macro days_until() -%}")
macro_end = speech.index("{%- endmacro -%}", macro_start) + len("{%- endmacro -%}")
speech_template = env.from_string(speech[macro_start:macro_end] + "{{ days_until() }}")
speech_names = {"mothers": "Mothers Day", "fathers": "Fathers Day", "easter": "Easter Sunday",
"thanksgiving_day": "Thanksgiving", "halloween": "Halloween", "chanukkah": "Chanukkah", "christmas": "Christmas"}
for name, spoken in speech_names.items():
for days in (0, 1, 5):
states = {f"sensor.{key}_countdown": "9999" for key in speech_names}
states[f"sensor.{name}_countdown"] = str(days)
result = speech_template.render().strip()
expected = f"Today is {spoken}!" if days == 0 else (f"{spoken} is tomorrow!" if days == 1 else f"days until {spoken}!")
if expected not in result:
raise CoverageError(f"speech {name}/{days}: missing {expected!r}, got {result!r}")
states = {f"sensor.{key}_countdown": "unavailable" for key in speech_names}
require(speech_template.render().strip(), "", "unavailable countdown speech")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=repo_root())

@ -18,48 +18,7 @@
"11/10": "Marine Corps Birthday",
"11/11": "Veterans Day",
"12/7": "Pearl Harbor Remembrance Day"
}
},
"dynamic": {
"01/18/2027": "Martin Luther King Jr. Day",
"01/17/2028": "Martin Luther King Jr. Day",
"01/15/2029": "Martin Luther King Jr. Day",
"01/21/2030": "Martin Luther King Jr. Day",
"01/20/2029": "Inauguration Day",
"01/20/2033": "Inauguration Day",
"02/15/2027": "Washington's Birthday (Presidents Day)",
"02/21/2028": "Washington's Birthday (Presidents Day)",
"02/19/2029": "Washington's Birthday (Presidents Day)",
"02/18/2030": "Washington's Birthday (Presidents Day)",
"05/16/2026": "Armed Forces Day",
"05/15/2027": "Armed Forces Day",
"05/20/2028": "Armed Forces Day",
"05/19/2029": "Armed Forces Day",
"05/18/2030": "Armed Forces Day",
"05/25/2026": "Memorial Day",
"05/31/2027": "Memorial Day",
"05/29/2028": "Memorial Day",
"05/28/2029": "Memorial Day",
"05/27/2030": "Memorial Day",
"09/07/2026": "Labor Day",
"09/06/2027": "Labor Day",
"09/04/2028": "Labor Day",
"09/03/2029": "Labor Day",
"09/02/2030": "Labor Day",
"09/18/2026": "National POW/MIA Recognition Day",
"09/17/2027": "National POW/MIA Recognition Day",
"09/15/2028": "National POW/MIA Recognition Day",
"09/21/2029": "National POW/MIA Recognition Day",
"09/20/2030": "National POW/MIA Recognition Day",
"10/12/2026": "Columbus Day",
"10/11/2027": "Columbus Day",
"10/09/2028": "Columbus Day",
"10/08/2029": "Columbus Day",
"10/14/2030": "Columbus Day",
"11/03/2026": "Election Day",
"11/02/2027": "Election Day",
"11/07/2028": "Election Day",
"11/06/2029": "Election Day",
"11/05/2030": "Election Day"
}
"dynamic": {}
}
}

@ -26,64 +26,6 @@
"12/25": "Christmas Day",
"12/31": "New Years Eve"
},
"dynamic": {
"03/29/2026": "Palm Sunday",
"04/03/2026": "Good Friday",
"04/05/2026": "Easter Sunday",
"04/20/2026": "Memorial Day",
"05/10/2026": "Mothers Day",
"06/21/2026": "Fathers Day",
"09/07/2026": "Labor Day",
"10/12/2026": "Columbus Day",
"11/26/2026": "Thanksgiving Day",
"01/18/2027": "MLK Day",
"02/10/2027": "Ash Wednesday",
"02/15/2027": "Presidents Day",
"03/21/2027": "Palm Sunday",
"03/26/2027": "Good Friday",
"03/28/2027": "Easter Sunday",
"04/19/2027": "Memorial Day",
"05/09/2027": "Mothers Day",
"06/20/2027": "Fathers Day",
"09/06/2027": "Labor Day",
"10/11/2027": "Columbus Day",
"11/25/2027": "Thanksgiving Day",
"01/17/2028": "MLK Day",
"02/21/2028": "Presidents Day",
"03/01/2028": "Ash Wednesday",
"04/09/2028": "Palm Sunday",
"04/14/2028": "Good Friday",
"04/16/2028": "Easter Sunday",
"04/17/2028": "Memorial Day",
"05/14/2028": "Mothers Day",
"06/18/2028": "Fathers Day",
"09/04/2028": "Labor Day",
"10/09/2028": "Columbus Day",
"11/23/2028": "Thanksgiving Day",
"01/15/2029": "MLK Day",
"02/14/2029": "Ash Wednesday",
"02/19/2029": "Presidents Day",
"03/25/2029": "Palm Sunday",
"03/30/2029": "Good Friday",
"04/01/2029": "Easter Sunday",
"04/23/2029": "Memorial Day",
"05/13/2029": "Mothers Day",
"06/17/2029": "Fathers Day",
"09/03/2029": "Labor Day",
"10/08/2029": "Columbus Day",
"11/22/2029": "Thanksgiving Day",
"01/21/2030": "MLK Day",
"02/18/2030": "Presidents Day",
"03/06/2030": "Ash Wednesday",
"04/14/2030": "Palm Sunday",
"04/19/2030": "Good Friday",
"04/21/2030": "Easter Sunday",
"04/22/2030": "Memorial Day",
"05/12/2030": "Mothers Day",
"06/16/2030": "Fathers Day",
"09/02/2030": "Labor Day",
"10/14/2030": "Columbus Day",
"11/28/2030": "Thanksgiving Day"
}
"dynamic": {}
}
}

@ -56,7 +56,7 @@ The kiosk package also exposes `sensor.kiosk_recent_phone_notifications`: a rest
| [![YAML source: climate](https://img.shields.io/static/v1?label=YAML&message=climate&color=lightgrey&logo=github&logoColor=181717)](climate.yaml) | Nest climate schedules plus 550-hour downstairs/450-hour upstairs/9-month AC filter reminders, native per-unit NFC tag change capture, per-unit change counts/cadence, and 360-day condenser-line cleanout reminders with snooze and changed/cleaned actions. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/nScl2JxYSB8) | `input_datetime.*_filter_snooze_until`, `sensor.*_ac_runtime_since_last_filter_change`, `sensor.*_ac_filter_days_since_last_change`, `sensor.*_ac_filter_change_count`, `sensor.hvac_condenser_lines_days_since_cleaned`, `script.notify_engine_two_button`, `mobile_app_notification_action`, `tag` |
| [![YAML source: garadget](https://img.shields.io/static/v1?label=YAML&message=garadget&color=lightgrey&logo=github&logoColor=181717)](garadget.yaml) | MQTT-based garage door control plus arrival helpers, entry prompts, camera context, and nighttime/away reminders with a shared one-hour phone and speech snooze. Garage-door snoozing resets after both doors close and does not survive restarts; the separate garbage-reminder snooze remains restart-safe. | `cover.large_garage_door`, `cover.small_garage_door`, `group.garage_doors`, `timer.garage_door_reminder_snooze`, `timer.garbage_reminder_snooze`, `script.open_large_garage_door_if_ready` |
| [![YAML source: august](https://img.shields.io/static/v1?label=YAML&message=august&color=lightgrey&logo=github&logoColor=181717)](august.yaml) | Front-door August smart lock with Alexa Show camera pop-up when unlocked. | `lock.front_door`, media_player actions for front doorbell camera |
| [![YAML source: holiday](https://img.shields.io/static/v1?label=YAML&message=holiday&color=lightgrey&logo=github&logoColor=181717)](holiday.yaml) | REST-driven US holiday + flag sensors plus the inspectable exterior lighting mode. | `sensor.holiday`, `sensor.flag`, `sensor.holiday_lighting_mode`, `sensor.holiday_lighting_scene`, JSON feed at `config/www/json_data/holidays.json` |
| [![YAML source: holiday](https://img.shields.io/static/v1?label=YAML&message=holiday&color=lightgrey&logo=github&logoColor=181717)](holiday.yaml) | Perpetual local holiday/flag rules, household fixed dates and one-off JSON additions, nine local countdowns, and a midnight refresh. Lighting shares the countdowns, calculates Mardi Gras from Easter, and covers eight Hanukkah evenings subject to flag-day priority. Existing speech entity IDs stay stable. | `sensor.holiday`, `sensor.flag`, `sensor.*_countdown`, `sensor.holiday_lighting_mode`, `sensor.holiday_lighting_scene`, [local calculator](../shell_scripts/holiday_countdowns.py) |
| [![YAML source: lightning](https://img.shields.io/static/v1?label=YAML&message=lightning&color=lightgrey&logo=github&logoColor=181717)](lightning.yaml) | Blitzortung lightning monitoring with guarded distance wording, snoozeable push actions, single-owner safety speech, and a 60-minute duplicate-trigger cooldown. | `sensor.blitzortung_lightning_counter`, `sensor.blitzortung_lightning_distance`, `input_boolean.snooze_lightning`, `script.speech_engine` |
| [![YAML source: logbook_activity_feed](https://img.shields.io/static/v1?label=YAML&message=logbook_activity_feed&color=lightgrey&logo=github&logoColor=181717)](logbook_activity_feed.yaml) | Dummy `sensor.activity_feed` + helper to write clean Activity entries (Issue #1550). | `sensor.activity_feed`, `script.send_to_logbook` |
| [![YAML source: mariadb_monitoring](https://img.shields.io/static/v1?label=YAML&message=mariadb_monitoring&color=lightgrey&logo=github&logoColor=181717)](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` |

@ -7,7 +7,10 @@
# Related Issue: 1774
# Centralizes holiday calendar data and the active exterior lighting mode.
# -------------------------------------------------------------------
# Notes: /local/json_data is served from config/www/json_data and drives lighting mode coverage.
# Notes: holiday_countdowns.py calculates floating dates and all nine countdowns locally.
# Notes: config/www/json_data holds fixed household dates and optional one-off additions.
# Notes: Existing IDs stay stable; calendars/countdowns refresh at startup and local midnight.
# Notes: Hanukkah lighting spans eight evenings, beginning before 25 Kislev; flag days retain priority.
######################################################################
# Video breakdown: https://www.vcloudinfo.com/2019/02/breaking-down-the-flag-sensor-in-home-assistant.html
# Modified for my own fun stuff!
@ -24,124 +27,100 @@ homeassistant:
icon: mdi:flag
friendly_name: Flag Day
###############################################################################
# Sensor updates once every 4 hours (14400 seconds) & runs 6 times in 24 hours
#
# First it checks for holiday in static section, if that doesn't exist,
# it checks in the dynamic section. If neither exists, the value will be empty.
###############################################################################
sensor:
- platform: rest
resource: http://localhost:8123/local/json_data/holidays.json
name: Holiday
scan_interval: 14400
value_template: >
{% set holiday_data = value_json.MAJOR_US if value_json is defined and value_json.MAJOR_US is defined else {} %}
{% set static_days = holiday_data.static if holiday_data.static is defined else {} %}
{% set dynamic_days = holiday_data.dynamic if holiday_data.dynamic is defined else {} %}
{% set today = now().month ~ '/' ~ now().day %}
{% set today_full = now().strftime('%m/%d/%Y') %}
{% set today_full_alt = now().month ~ '/' ~ now().day ~ '/' ~ now().year %}
{% if today in static_days %}
{{ static_days[today] }}
{% elif today_full in dynamic_days %}
{{ dynamic_days[today_full] }}
{% elif today_full_alt in dynamic_days %}
{{ dynamic_days[today_full_alt] }}
{% endif %}
json_attributes:
- MAJOR_US
################################################################################
# Sensor Uses Flag data generated by AI
################################################################################
- platform: rest
resource: http://localhost:8123/local/json_data/flag_days.json
name: Flag
scan_interval: 14400
value_template: >-
{% set now_string = now().month ~ '/' ~ now().day %}
{% set now_full_string = now().strftime('%m/%d/%Y') %}
{% set now_full_alt_string = now().month ~ '/' ~ now().day ~ '/' ~ now().year %}
{% set flag_data = value_json.Flag_Days_US if value_json is defined and value_json.Flag_Days_US is defined else {} %}
{% set static_days = flag_data.static if flag_data.static is defined else {} %}
{% set dynamic_days = flag_data.dynamic if flag_data.dynamic is defined else {} %}
{% if now_string in static_days %}
True
{% elif now_full_string in dynamic_days or now_full_alt_string in dynamic_days %}
True
{% else %}
False
{% endif %}
json_attributes:
- Flag_Days_US
################################################################################
# Countdown Sensor using WolfRam Alpha Natural language queries
################################################################################
- platform: rest
name: Halloween Countdown
resource: !secret wolframalpha_halloween_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Christmas Countdown
resource: !secret wolframalpha_xmas_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Easter Countdown
resource: !secret wolframalpha_easter_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Mothers Countdown
resource: !secret wolframalpha_mothersday_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Fathers Countdown
resource: !secret wolframalpha_fathersday_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Chanukkah Countdown
resource: !secret wolframalpha_chanukkah_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Labor Day Countdown
resource: !secret wolframalpha_labor_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Memorial Day Countdown
resource: !secret wolframalpha_memorial_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
- platform: rest
name: Thanksgiving Day Countdown
resource: !secret wolframalpha_thanksgiving_api
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
command_line:
- sensor:
name: Holiday
unique_id: holiday_calendar
command: "python3 /config/shell_scripts/holiday_countdowns.py holiday"
value_template: "{{ value_json.state }}"
json_attributes:
- MAJOR_US
scan_interval: 86400
- sensor:
name: Flag
unique_id: flag_calendar
command: "python3 /config/shell_scripts/holiday_countdowns.py flag"
value_template: "{{ value_json.state }}"
json_attributes:
- Flag_Days_US
scan_interval: 86400
- sensor:
name: Halloween Countdown
unique_id: halloween_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py halloween"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Christmas Countdown
unique_id: christmas_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py christmas"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Easter Countdown
unique_id: easter_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py easter"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Mothers Countdown
unique_id: mothers_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py mothers"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Fathers Countdown
unique_id: fathers_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py fathers"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Chanukkah Countdown
unique_id: chanukkah_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py chanukkah"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Labor Day Countdown
unique_id: labor_day_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py labor_day"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Memorial Day Countdown
unique_id: memorial_day_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py memorial_day"
unit_of_measurement: Days
scan_interval: 86400
- sensor:
name: Thanksgiving Day Countdown
unique_id: thanksgiving_day_countdown
command: "python3 /config/shell_scripts/holiday_countdowns.py thanksgiving_day"
unit_of_measurement: Days
scan_interval: 86400
automation:
- alias: Holiday Countdown Midnight Refresh
id: holiday_countdown_midnight_refresh
mode: single
triggers:
- trigger: time
at: "00:00:05"
actions:
- action: homeassistant.update_entity
target:
entity_id:
- sensor.holiday
- sensor.flag
- sensor.halloween_countdown
- sensor.christmas_countdown
- sensor.easter_countdown
- sensor.mothers_countdown
- sensor.fathers_countdown
- sensor.chanukkah_countdown
- sensor.labor_day_countdown
- sensor.memorial_day_countdown
- sensor.thanksgiving_day_countdown
template:
- sensor:
@ -149,75 +128,60 @@ template:
unique_id: holiday_lighting_mode
icon: mdi:string-lights
state: >-
{%- set today = today_at('00:00') -%}
{%- set today = now().date() -%}
{%- set mmdd = now().strftime('%m%d') | int -%}
{%- set holiday_data = state_attr('sensor.holiday', 'MAJOR_US') or {} -%}
{%- set flag_data = state_attr('sensor.flag', 'Flag_Days_US') or {} -%}
{%- set holiday_dynamic = holiday_data.get('dynamic', {}) if holiday_data is mapping else {} -%}
{%- set flag_dynamic = flag_data.get('dynamic', {}) if flag_data is mapping else {} -%}
{%- set days_to = namespace(easter=9999, mothers=9999, fathers=9999, memorial=9999, labor=9999, thanksgiving=9999) -%}
{%- set dates = namespace(mardi_gras=false, hanukkah=false) -%}
{%- set mode = namespace(value='standard') -%}
{%- for date_text, name in holiday_dynamic.items() -%}
{%- set event_date = strptime(date_text, '%m/%d/%Y') -%}
{%- set days = ((as_timestamp(event_date) - as_timestamp(today)) / 86400) | int -%}
{%- if days >= 0 and name == 'Easter Sunday' and days < days_to.easter -%}
{%- set days_to.easter = days -%}
{%- elif days >= 0 and name == 'Mothers Day' and days < days_to.mothers -%}
{%- set days_to.mothers = days -%}
{%- elif days >= 0 and name == 'Fathers Day' and days < days_to.fathers -%}
{%- set days_to.fathers = days -%}
{%- elif days >= 0 and name == 'Thanksgiving Day' and days < days_to.thanksgiving -%}
{%- set days_to.thanksgiving = days -%}
{%- endif -%}
{%- endfor -%}
{%- for date_text, name in flag_dynamic.items() -%}
{%- set event_date = strptime(date_text, '%m/%d/%Y') -%}
{%- set days = ((as_timestamp(event_date) - as_timestamp(today)) / 86400) | int -%}
{%- if days >= 0 and name == 'Memorial Day' and days < days_to.memorial -%}
{%- set days_to.memorial = days -%}
{%- elif days >= 0 and name == 'Labor Day' and days < days_to.labor -%}
{%- set days_to.labor = days -%}
{%- for date_text, names in holiday_dynamic.items() -%}
{%- set event_date = strptime(date_text, '%m/%d/%Y', none) -%}
{%- if event_date is not none -%}
{%- set days = (event_date.date() - today).days -%}
{%- if 'Mardi Gras' in names.split('; ') and days == 0 -%}
{%- set dates.mardi_gras = true -%}
{%- elif 'Chanukkah' in names.split('; ') and -6 <= days <= 1 -%}
{%- set dates.hanukkah = true -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- set christmas = strptime(now().year ~ '-12-25', '%Y-%m-%d') -%}
{%- set christmas_days = ((as_timestamp(christmas) - as_timestamp(today)) / 86400) | int -%}
{%- if is_state('sensor.flag', 'True') -%}
{%- set mode.value = 'RWB' -%}
{%- elif mmdd == 101 -%}
{%- set mode.value = 'new_years_day' -%}
{%- elif dates.mardi_gras -%}
{%- set mode.value = 'mardi_gras' -%}
{%- elif mmdd >= 210 and mmdd <= 214 -%}
{%- set mode.value = 'valentine' -%}
{%- elif mmdd == 305 -%}
{%- set mode.value = 'mardi_gras' -%}
{%- elif mmdd == 314 -%}
{%- set mode.value = 'pi' -%}
{%- elif mmdd >= 315 and mmdd <= 317 -%}
{%- set mode.value = 'st_patty' -%}
{%- elif days_to.easter < 4 -%}
{%- elif 0 <= states('sensor.easter_countdown') | int(9999) < 4 -%}
{%- set mode.value = 'easter' -%}
{%- elif mmdd == 504 -%}
{%- set mode.value = 'starwars' -%}
{%- elif mmdd == 505 -%}
{%- set mode.value = 'cinco_de_mayo' -%}
{%- elif days_to.mothers < 4 -%}
{%- elif 0 <= states('sensor.mothers_countdown') | int(9999) < 4 -%}
{%- set mode.value = 'mothers_day' -%}
{%- elif days_to.fathers < 4 -%}
{%- elif 0 <= states('sensor.fathers_countdown') | int(9999) < 4 -%}
{%- set mode.value = 'fathers_day' -%}
{%- elif days_to.memorial < 3 -%}
{%- elif 0 <= states('sensor.memorial_day_countdown') | int(9999) < 3 -%}
{%- set mode.value = 'RWB' -%}
{%- elif mmdd == 704 -%}
{%- set mode.value = 'RWB' -%}
{%- elif days_to.labor < 3 -%}
{%- elif 0 <= states('sensor.labor_day_countdown') | int(9999) < 3 -%}
{%- set mode.value = 'RWB' -%}
{%- elif mmdd >= 1001 and mmdd <= 1031 -%}
{%- set mode.value = 'halloween' -%}
{%- elif mmdd == 1111 -%}
{%- set mode.value = 'veterans' -%}
{%- elif days_to.thanksgiving < 4 -%}
{%- elif 0 <= states('sensor.thanksgiving_day_countdown') | int(9999) < 4 -%}
{%- set mode.value = 'thanksgiving' -%}
{%- elif states('sensor.chanukkah_countdown') | int(9999) <= 1 -%}
{%- elif dates.hanukkah -%}
{%- set mode.value = 'hanukkah' -%}
{%- elif christmas_days >= 0 and christmas_days <= 25 -%}
{%- elif 0 <= states('sensor.christmas_countdown') | int(9999) <= 25 -%}
{%- set mode.value = 'christmas' -%}
{%- elif mmdd == 1231 -%}
{%- set mode.value = 'new_years_day' -%}
@ -238,4 +202,4 @@ template:
mode: "{{ states('sensor.holiday_lighting_mode') }}"
holiday: "{{ states('sensor.holiday') }}"
flag_day: "{{ states('sensor.flag') }}"
source: config/www/json_data holiday and flag calendars
source: local calendar rules plus config/www/json_data household dates

@ -27,6 +27,7 @@ Longer-running shell helpers referenced by automations, packages, or cron. Anyth
| File | Why it matters |
| --- | --- |
| [HAUpdate.sh](HAUpdate.sh) | One-command Home Assistant update helper. |
| [holiday_countdowns.py](holiday_countdowns.py) | Local countdowns and perpetual holiday/flag calendars using weekday rules, python-dateutil for Gregorian Easter, and hdate for 25 Kislev. Reads fixed household dates and optional one-off `dynamic` additions from `www/json_data`; preserves colliding event names. Uses the container's local timezone and installed libraries; no API key or network call. Actual dates are used, rather than substitute workdays. Preview with `python3 holiday_countdowns.py chanukkah --date 2026-10-15`. |
| [apt_pending_check.sh](apt_pending_check.sh) | On-demand, read-only APT pending-count reporter for the Docker host maintenance dashboard. |
| [apt_weekly.sh](apt_weekly.sh) | Daily APT check-then-update workflow that posts webhook status and can schedule reboot when needed. |
| [apt_reboot_report.sh](apt_reboot_report.sh) | Boot-time status reporter that reuses the registered APT webhook, retries while HA starts, and confirms docker_10 again after a startup-settle delay so its co-hosted HA reboot state clears reliably. |
@ -36,6 +37,8 @@ Longer-running shell helpers referenced by automations, packages, or cron. Anyth
| [![YAML source: basketball](https://img.shields.io/static/v1?label=YAML&message=basketball&color=lightgrey&logo=github&logoColor=181717)](basketball.yaml) | ESPN stat scraping helper used by sensors. |
| [Jinja Code.py](Jinja Code.py) | Reference Jinja snippets for templating. |
The holiday calculator uses actual calendar dates, rather than substitute weekdays for work holidays. In `www/json_data/holidays.json` and `www/json_data/flag_days.json`, `static` maps `M/D` to fixed household dates; `dynamic` accepts optional one-off `MM/DD/YYYY` additions. Floating events come from the calculator and appear in the existing sensor attributes. The older `config/json_data` copies are not read at runtime. Calendar, lighting-boundary, and speech checks run in the existing holiday validation gate.
### Tips
- The TeslaMate snapshot helper reads its private Grafana render URL and authentication data from local secrets. Grafana must have an image renderer configured. The `Tesla Visited Map` Local file camera points to `image/teslamate_visited.png` under the Home Assistant configuration directory. The hourly sensor checks the cache and refreshes after 06:00 America/New_York; its timestamp and `stale`/`last_error` attributes expose refresh health. Cached images and credentials remain outside source control.

@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Calculate household holiday calendars and countdowns without network requests.
Uses python-dateutil and hdate from the Home Assistant container. The container's
local timezone supplies today's date; --date supports operator previews.
Reads fixed dates and one-off additions from www/json_data; floating dates never
need an annual table update. Chanukkah targets the first daytime date (25 Kislev).
Its first candle-lighting evening is the preceding Gregorian date.
"""
from __future__ import annotations
import argparse
import calendar
import json
from datetime import date, datetime, timedelta
from pathlib import Path
from dateutil.easter import easter
def nth_weekday(year: int, month: int, weekday: int, occurrence: int) -> date:
"""Return an occurrence of a weekday, with Monday numbered zero."""
first = date(year, month, 1)
return first + timedelta(days=(weekday - first.weekday()) % 7 + 7 * (occurrence - 1))
def memorial_day(year: int) -> date:
"""Return the last Monday in May, rather than an observed substitute date."""
last = date(year, 5, calendar.monthrange(year, 5)[1])
return last - timedelta(days=last.weekday())
GREGORIAN_RULES = {
"halloween": lambda year: date(year, 10, 31),
"christmas": lambda year: date(year, 12, 25),
"easter": easter,
"mothers": lambda year: nth_weekday(year, 5, 6, 2),
"fathers": lambda year: nth_weekday(year, 6, 6, 3),
"labor_day": lambda year: nth_weekday(year, 9, 0, 1),
"memorial_day": memorial_day,
"thanksgiving_day": lambda year: nth_weekday(year, 11, 3, 4),
}
# Actual calendar dates, not substitute weekdays used for employee time off.
HOLIDAY_RULES = {
"MLK Day": lambda year: nth_weekday(year, 1, 0, 3),
"Presidents Day": lambda year: nth_weekday(year, 2, 0, 3),
"Mardi Gras": lambda year: easter(year) - timedelta(days=47),
"Ash Wednesday": lambda year: easter(year) - timedelta(days=46),
"Palm Sunday": lambda year: easter(year) - timedelta(days=7),
"Good Friday": lambda year: easter(year) - timedelta(days=2),
"Easter Sunday": GREGORIAN_RULES["easter"],
"Mothers Day": GREGORIAN_RULES["mothers"],
"Fathers Day": GREGORIAN_RULES["fathers"],
"Memorial Day": GREGORIAN_RULES["memorial_day"],
"Labor Day": GREGORIAN_RULES["labor_day"],
"Columbus Day": lambda year: nth_weekday(year, 10, 0, 2),
"Thanksgiving Day": GREGORIAN_RULES["thanksgiving_day"],
}
FLAG_RULES = {
"Martin Luther King Jr. Day": HOLIDAY_RULES["MLK Day"],
"Washington's Birthday (Presidents Day)": HOLIDAY_RULES["Presidents Day"],
"Armed Forces Day": lambda year: nth_weekday(year, 5, 5, 3),
"Memorial Day": GREGORIAN_RULES["memorial_day"],
"Labor Day": GREGORIAN_RULES["labor_day"],
"National POW/MIA Recognition Day": lambda year: nth_weekday(year, 9, 4, 3),
"Columbus Day": HOLIDAY_RULES["Columbus Day"],
"Election Day": lambda year: nth_weekday(year, 11, 0, 1) + timedelta(days=1),
}
def chanukkah_in_year(year: int) -> date:
"""Return 25 Kislev falling in the requested Gregorian year."""
from hdate.hebrew_date import HebrewDate, Months
hebrew_year = HebrewDate.from_gdate(date(year, 1, 1)).year
target = HebrewDate(hebrew_year, Months.KISLEV, 25).to_gdate()
if target.year < year:
target = HebrewDate(hebrew_year + 1, Months.KISLEV, 25).to_gdate()
return target
def calendar_data(kind: str, today: date, data_dir: Path | None = None) -> dict:
"""Merge perpetual rules with household fixed dates and one-off additions.
Attribute names stay compatible with the former REST sensors. Three years
cover next-year lookahead and Hanukkah evenings that cross New Year's Day.
Multiple events sharing a date are retained rather than silently overwritten.
"""
data_dir = data_dir or Path(__file__).resolve().parents[1] / "www" / "json_data"
filename, attribute, rules = (
("holidays.json", "MAJOR_US", HOLIDAY_RULES)
if kind == "holiday"
else ("flag_days.json", "Flag_Days_US", FLAG_RULES)
)
source = json.loads((data_dir / filename).read_text(encoding="utf-8"))[attribute]
events: dict[date, list[str]] = {}
for year in range(today.year - 1, today.year + 2):
for name, rule in rules.items():
events.setdefault(rule(year), []).append(name)
if kind == "holiday":
events.setdefault(chanukkah_in_year(year), []).append("Chanukkah")
elif year % 4 == 1:
events.setdefault(date(year, 1, 20), []).append("Inauguration Day")
for key, name in source.get("dynamic", {}).items():
month, day, year = map(int, key.split("/"))
names = events.setdefault(date(year, month, day), [])
if name not in names:
names.append(name)
dynamic = {day.strftime("%m/%d/%Y"): "; ".join(names) for day, names in sorted(events.items())}
fixed_name = source["static"].get(f"{today.month}/{today.day}", "")
event_name = dynamic.get(today.strftime("%m/%d/%Y"), "")
state = (fixed_name or event_name) if kind == "holiday" else str(bool(fixed_name or event_name))
return {"state": state, attribute: {"static": source["static"], "dynamic": dynamic}}
def next_holiday(holiday: str, today: date) -> date:
"""Find the next holiday date, including today when it is the holiday."""
if holiday == "chanukkah":
target = chanukkah_in_year(today.year)
if target < today:
target = chanukkah_in_year(today.year + 1)
return target
rule = GREGORIAN_RULES[holiday]
target = rule(today.year)
return rule(today.year + 1) if target < today else target
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("holiday", choices=sorted([*GREGORIAN_RULES, "chanukkah", "holiday", "flag"]))
parser.add_argument("--date", type=date.fromisoformat, help="Preview an ISO date")
args = parser.parse_args()
today = args.date or datetime.now().astimezone().date()
if args.holiday in ("holiday", "flag"):
print(json.dumps(calendar_data(args.holiday, today)))
else:
print((next_holiday(args.holiday, today) - today).days)
if __name__ == "__main__":
main()

@ -10,6 +10,7 @@
# Generates macro prompts for weather, reminders, and AI-driven speech routines.
# -------------------------------------------------------------------
# Weather, responsibilities, holidays, air quality, and fact prompts parsed by speech_processing/speech_engine.
# Notes: Holiday countdown zero means today, one means tomorrow; unavailable dates stay silent.
# Notes: The nearest Airly CAQI is the primary local reading and stays silent
# below medium (50). Pirate Weather US AQI is a safety backstop at 101+.
# Notes: Person state `home` means someone is physically at Bear Stone.
@ -292,34 +293,37 @@
{%- endmacro -%}
{%- macro holiday() -%}
{% if states('sensor.holiday') != '' %}
{% if has_value('sensor.holiday') and states('sensor.holiday') | trim != '' %}
Today is {{ states('sensor.holiday') }}.
{% endif %}
{%- endmacro -%}
{# YOUTUBE VIDEO ********* https://www.vcloudinfo.com/2019/11/adding-days-until-sensor-to-my-home-assistant-speech-routines.html #}
{%- macro days_until() -%}
{%- if states('sensor.mothers_countdown') | int(9999)< 20 -%}
and don't forget, there is only {{ states('sensor.mothers_countdown') }} days until Mothers day!
{%- elif states('sensor.fathers_countdown') | int(9999)< 20 -%}
and don't forget, there are {{ states('sensor.fathers_countdown') }} days until Fathers day!
{%- elif states('sensor.easter_countdown') | int(9999)< 15 -%}
and don't forget, there are {{ states('sensor.easter_countdown') }} colorful days until Easter Sunday!
{%- elif states('sensor.thanksgiving_day_countdown') | int(9999)< 10 and states('sensor.thanksgiving_day_countdown') | int(9999)> 0 -%}
and don't forget, there are {{ states('sensor.thanksgiving_day_countdown') }} thankful days until Thanksgiving
{%- elif states('sensor.thanksgiving_day_countdown') | int(9999)< 1 -%}
and don't forget, Thanksgiving is tomorrow!
{%- elif states('sensor.halloween_countdown') | int(9999)< 30 and states('sensor.halloween_countdown') | int(9999)> 0 -%}
and don't forget, there are {{ states('sensor.halloween_countdown') }} Spooky days until Halloween!
{%- elif states('sensor.halloween_countdown') | int(9999)< 1 -%}
and don't forget, Tomorrow is Halloween!
{%- elif states('sensor.chanukkah_countdown') | int(9999)< 15 -%}
and don't forget, there are {{ states('sensor.chanukkah_countdown') }} days until Chanukkah!
{%- elif states('sensor.christmas_countdown') | int(9999)< 30 and states('sensor.christmas_countdown') | int(9999)> 0 -%}
and don't forget, there are {{ states('sensor.christmas_countdown') }} Merry days until Christmas!
{%- elif states('sensor.christmas_countdown') | int(9999)< 1 -%}
and don't forget, Santa Claus is coming tomorrow!
{% endif %}
{%- set reminder = namespace(message='') -%}
{%- for entity, name, limit, adjective in [
('sensor.mothers_countdown', 'Mothers Day', 20, ''),
('sensor.fathers_countdown', 'Fathers Day', 20, ''),
('sensor.easter_countdown', 'Easter Sunday', 15, 'colorful '),
('sensor.thanksgiving_day_countdown', 'Thanksgiving', 10, 'thankful '),
('sensor.halloween_countdown', 'Halloween', 30, 'Spooky '),
('sensor.chanukkah_countdown', 'Chanukkah', 15, ''),
('sensor.christmas_countdown', 'Christmas', 30, 'Merry ')
] -%}
{%- set days = states(entity) | int(9999) -%}
{%- if reminder.message == '' and 0 <= days < limit -%}
{%- if days == 0 -%}
{%- set reminder.message = 'Today is ' ~ name ~ '!' -%}
{%- elif days == 1 -%}
{%- set reminder.message = name ~ ' is tomorrow!' -%}
{%- else -%}
{%- set reminder.message = 'there are ' ~ days ~ ' ' ~ adjective ~ 'days until ' ~ name ~ '!' -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- if reminder.message != '' %}
and don't forget, {{ reminder.message }}
{% endif -%}
{%- endmacro -%}
{# friendly duration/location helper #}

@ -78,15 +78,6 @@ cloud_user_pool: 'SuperSecretPlace'
cloud_relayer: 'SuperSecretWebThingy'
vantage_api: LONGAPIKEYFORFREE
minecraft: private_server.com
wolframalpha_halloween_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20halloween
wolframalpha_xmas_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20christmas
wolframalpha_easter_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20easter
wolframalpha_mothersday_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20mothers%20day
wolframalpha_fathersday_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20fathers%20day
wolframalpha_chanukkah_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20chanukkah
wolframalpha_labor_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%labor%20day
wolframalpha_memorial_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20memorial
wolframalpha_thanksgiving_api: https://api.wolframalpha.com/v2/result?appid=JIUY8U-4V8KY45VT1&i=How%20many%20days%20until%20thanksgiving
bearclaw_status_url: http://192.168.10.13:8124/api/bearclaw/status
bgw_probe_status_url: http://127.0.0.1:8181/api/v1/status
bearclaw_duplicati_verify_url: http://192.168.10.13:8124/api/admin/actions/duplicati-verify

@ -12,71 +12,6 @@
"11/11": "Veterans Day",
"12/7": "Pearl Harbor Remembrance Day"
},
"dynamic": {
"01/20/2025": "Inauguration Day",
"01/16/2023": "Martin Luther King Jr. Day",
"01/15/2024": "Martin Luther King Jr. Day",
"01/19/2026": "Martin Luther King Jr. Day",
"01/18/2027": "Martin Luther King Jr. Day",
"01/17/2028": "Martin Luther King Jr. Day",
"01/15/2029": "Martin Luther King Jr. Day",
"01/21/2030": "Martin Luther King Jr. Day",
"02/20/2023": "Washington's Birthday (Presidents Day)",
"02/19/2024": "Washington's Birthday (Presidents Day)",
"02/17/2025": "Washington's Birthday (Presidents Day)",
"02/16/2026": "Washington's Birthday (Presidents Day)",
"02/15/2027": "Washington's Birthday (Presidents Day)",
"02/21/2028": "Washington's Birthday (Presidents Day)",
"02/19/2029": "Washington's Birthday (Presidents Day)",
"02/18/2030": "Washington's Birthday (Presidents Day)",
"05/20/2023": "Armed Forces Day",
"05/18/2024": "Armed Forces Day",
"05/17/2025": "Armed Forces Day",
"05/16/2026": "Armed Forces Day",
"05/15/2027": "Armed Forces Day",
"05/20/2028": "Armed Forces Day",
"05/19/2029": "Armed Forces Day",
"05/18/2030": "Armed Forces Day",
"05/29/2023": "Memorial Day",
"05/27/2024": "Memorial Day",
"05/26/2025": "Memorial Day",
"05/25/2026": "Memorial Day",
"05/31/2027": "Memorial Day",
"05/29/2028": "Memorial Day",
"05/28/2029": "Memorial Day",
"05/27/2030": "Memorial Day",
"09/04/2023": "Labor Day",
"09/02/2024": "Labor Day",
"09/01/2025": "Labor Day",
"09/07/2026": "Labor Day",
"09/06/2027": "Labor Day",
"09/04/2028": "Labor Day",
"09/03/2029": "Labor Day",
"09/02/2030": "Labor Day",
"09/15/2023": "National POW/MIA Recognition Day",
"09/20/2024": "National POW/MIA Recognition Day",
"09/19/2025": "National POW/MIA Recognition Day",
"09/18/2026": "National POW/MIA Recognition Day",
"09/17/2027": "National POW/MIA Recognition Day",
"09/15/2028": "National POW/MIA Recognition Day",
"09/21/2029": "National POW/MIA Recognition Day",
"09/20/2030": "National POW/MIA Recognition Day",
"10/09/2023": "Columbus Day",
"10/14/2024": "Columbus Day",
"10/13/2025": "Columbus Day",
"10/12/2026": "Columbus Day",
"10/11/2027": "Columbus Day",
"10/09/2028": "Columbus Day",
"10/08/2029": "Columbus Day",
"10/14/2030": "Columbus Day",
"11/07/2023": "Election Day",
"11/05/2024": "Election Day",
"11/04/2025": "Election Day",
"11/03/2026": "Election Day",
"11/02/2027": "Election Day",
"11/07/2028": "Election Day",
"11/06/2029": "Election Day",
"11/05/2030": "Election Day"
}
"dynamic": {}
}
}

@ -25,92 +25,6 @@
"12/25": "Christmas Day",
"12/31": "New Years Eve"
},
"dynamic": {
"11/23/2023": "Thanksgiving Day",
"01/15/2024": "MLK Day",
"02/14/2024": "Ash Wednesday",
"02/19/2024": "Presidents Day",
"03/24/2024": "Palm Sunday",
"03/29/2024": "Good Friday",
"03/31/2024": "Easter Sunday",
"04/22/2024": "Memorial Day",
"05/12/2024": "Mothers Day",
"06/16/2024": "Fathers Day",
"09/02/2024": "Labor Day",
"10/14/2024": "Columbus Day",
"11/28/2024": "Thanksgiving Day",
"01/20/2025": "MLK Day",
"02/17/2025": "Presidents Day",
"03/05/2025": "Ash Wednesday",
"04/13/2025": "Palm Sunday",
"04/18/2025": "Good Friday",
"04/20/2025": "Easter Sunday",
"04/21/2025": "Memorial Day",
"05/11/2025": "Mothers Day",
"06/15/2025": "Fathers Day",
"09/01/2025": "Labor Day",
"10/13/2025": "Columbus Day",
"11/27/2025": "Thanksgiving Day",
"01/19/2026": "MLK Day",
"02/16/2026": "Presidents Day",
"02/18/2026": "Ash Wednesday",
"03/29/2026": "Palm Sunday",
"04/03/2026": "Good Friday",
"04/05/2026": "Easter Sunday",
"04/20/2026": "Memorial Day",
"05/10/2026": "Mothers Day",
"06/21/2026": "Fathers Day",
"09/07/2026": "Labor Day",
"10/12/2026": "Columbus Day",
"11/26/2026": "Thanksgiving Day",
"01/18/2027": "MLK Day",
"02/10/2027": "Ash Wednesday",
"02/15/2027": "Presidents Day",
"03/21/2027": "Palm Sunday",
"03/26/2027": "Good Friday",
"03/28/2027": "Easter Sunday",
"04/19/2027": "Memorial Day",
"05/09/2027": "Mothers Day",
"06/20/2027": "Fathers Day",
"09/06/2027": "Labor Day",
"10/11/2027": "Columbus Day",
"11/25/2027": "Thanksgiving Day",
"01/17/2028": "MLK Day",
"02/21/2028": "Presidents Day",
"03/01/2028": "Ash Wednesday",
"04/09/2028": "Palm Sunday",
"04/14/2028": "Good Friday",
"04/16/2028": "Easter Sunday",
"04/17/2028": "Memorial Day",
"05/14/2028": "Mothers Day",
"06/18/2028": "Fathers Day",
"09/04/2028": "Labor Day",
"10/09/2028": "Columbus Day",
"11/23/2028": "Thanksgiving Day",
"01/15/2029": "MLK Day",
"02/14/2029": "Ash Wednesday",
"02/19/2029": "Presidents Day",
"03/25/2029": "Palm Sunday",
"03/30/2029": "Good Friday",
"04/01/2029": "Easter Sunday",
"04/23/2029": "Memorial Day",
"05/13/2029": "Mothers Day",
"06/17/2029": "Fathers Day",
"09/03/2029": "Labor Day",
"10/08/2029": "Columbus Day",
"11/22/2029": "Thanksgiving Day",
"01/21/2030": "MLK Day",
"02/18/2030": "Presidents Day",
"03/06/2030": "Ash Wednesday",
"04/14/2030": "Palm Sunday",
"04/19/2030": "Good Friday",
"04/21/2030": "Easter Sunday",
"04/22/2030": "Memorial Day",
"05/12/2030": "Mothers Day",
"06/16/2030": "Fathers Day",
"09/02/2030": "Labor Day",
"10/14/2030": "Columbus Day",
"11/28/2030": "Thanksgiving Day"
}
"dynamic": {}
}
}

@ -1 +1,4 @@
PyYAML==6.0.3
python-dateutil==2.9.0.post0
hdate==1.2.2
Jinja2==3.1.6

Loading…
Cancel
Save

Powered by TurnKey Linux.