Add HVAC and holiday lighting helpers

This commit is contained in:
Carlo Costanzo
2026-06-29 17:06:27 -04:00
parent ea5efa977d
commit 97c4ee6510
8 changed files with 500 additions and 154 deletions
@@ -17,8 +17,19 @@
entity: sensor.upstairs_ac_runtime_since_last_filter_change
- type: tile
entity: input_datetime.downstairs_last_filter_change
tap_action:
action: more-info
entity: script.reset_downstairs_filter
- type: tile
entity: input_datetime.upstairs_last_filter_change
tap_action:
action: more-info
entity: script.reset_upstairs_filter
- type: tile
entity: input_datetime.hvac_condenser_lines_last_cleaned
tap_action:
action: more-info
entity: script.reset_hvac_condenser_lines_cleaned
- type: custom:power-flow-card-plus
entities:
battery:
+5 -5
View File
@@ -41,10 +41,10 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
| [alarm.yaml](alarm.yaml) | NodeMCU-powered perimeter monitoring with arm/disarm helpers and rich notifications. | `binary_sensor.mcu*_gpio*`, `group.family`, notify + siren scripts |
| [alexa_media_player.yaml](alexa_media_player.yaml) | Alexa Media helper sensors including stable bedroom wake-alarm wrappers for Carlo and Stacey plus a combined next-wake view. | `sensor.last_alexa`, `sensor.bedroom_next_wake_alarm`, `sensor.bedroom_next_wake_alarm_source`, `binary_sensor.bedroom_next_wake_alarm_active` |
| [fridge.yaml](fridge.yaml) | SmartThinQ fridge monitoring with 6-minute raw door-open alerts plus fridge/freezer status announcements. | `binary_sensor.refrigerator_door_open`, `script.notify_engine`, `script.speech_engine` |
| [climate.yaml](climate.yaml) | Nest climate schedules plus runtime-based AC filter reminders with snooze and filter-changed actions. | `input_datetime.*_filter_snooze_until`, `script.notify_engine_two_button`, mobile app action events |
| [climate.yaml](climate.yaml) | Nest climate schedules plus 350-hour/9-month AC filter reminders and 360-day condenser-line cleanout reminders with snooze and changed/cleaned actions. | `input_datetime.*_filter_snooze_until`, `sensor.*_ac_runtime_since_last_filter_change`, `input_datetime.hvac_condenser_lines_last_cleaned`, `script.notify_engine_two_button`, mobile app action events |
| [garadget.yaml](garadget.yaml) | MQTT-based garage door control plus arrival helpers, entry prompts, wind checks, nighttime reminders, and camera context. | `cover.large_garage_door`, `cover.small_garage_door`, `group.garage_doors`, `script.open_large_garage_door_if_ready` |
| [august.yaml](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 |
| [holiday.yaml](holiday.yaml) | REST-driven US holiday + flag sensors that color scenes and exterior lighting. | `sensor.holiday`, `sensor.flag`, JSON feed at `config/json_data/holidays.json` |
| [holiday.yaml](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` |
| [lightning.yaml](lightning.yaml) | Blitzortung lightning counter monitoring with snoozeable push actions. | `sensor.blitzortung_lightning_counter`, `input_boolean.snooze_lightning`, notify engine actions |
| [logbook_activity_feed.yaml](logbook_activity_feed.yaml) | Dummy `sensor.activity_feed` + helper to write clean Activity entries (Issue #1550). | `sensor.activity_feed`, `script.send_to_logbook` |
| [mariadb_monitoring.yaml](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` |
@@ -66,7 +66,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
| [vacation_mode.yaml](vacation_mode.yaml) | Auto-enable vacation mode after 24 hours away or no bed use, track sitter analytics/secure-house checks, stale-visit timeout protection, deliver sitter-facing briefings, and send 10 AM/10 PM house digests with Powerwall/security/backup status plus Joanna review. | `input_boolean.vacation_mode`, `input_boolean.house_sitter_present`, `input_datetime.vacation_house_sitter_*`, `input_datetime.vacation_house_status_digest_last_sent`, `sensor.vacation_house_sitter_*`, `binary_sensor.powerwall_grid_status`, `sensor.powerwall_charge`, `group.garage_doors`, `lock.front_door`, `script.vacation_house_sitter_clear_presence`, `script.vacation_house_status_digest`, `script.notify_engine`, `rest_command.bearclaw_command`, `script.joanna_send_telegram` |
| [maintenance_log.yaml](maintenance_log.yaml) | Joanna maintenance webhook ingest for water softener salt with idempotent event handling, Activity feed logging, and recorder-backed helper history for long-term graphing. | `automation.maintenance_log_joanna_webhook_ingest`, `input_number.water_softener_salt_total_added_lb`, `counter.water_softener_salt_event_count`, `sensor.water_softener_salt_days_since_last_add` |
| [kiosk_tablet.yaml](kiosk_tablet.yaml) | Keeps the bedroom Fully Kiosk Fire tablet pinned to the dedicated camera kiosk dashboard when the page or foreground app drifts; normal wake events only restore brightness/focus. See the [video walkthrough](https://youtu.be/ChgEu0IDWzc). | `sensor.alarm_panel_1_current_page`, `sensor.alarm_panel_1_foreground_app`, `button.alarm_panel_1_bring_to_foreground`, `button.alarm_panel_1_load_start_url` |
| [powerwall.yaml](powerwall.yaml) | Track Tesla Powerwall grid status, push live outage tracking to mobile targets, and shed loads automatically when off-grid (alerts include Activity feed + Repairs). | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `script.notify_live_activity`, `repairs.create` |
| [powerwall.yaml](powerwall.yaml) | Track Tesla Powerwall grid status, push live outage tracking to mobile targets, shed loads automatically when off-grid, and alert if the Powerwall stays low after grid power returns; see the [watchdog video](https://youtu.be/hR_0lFEE2bA) and [companion post](https://www.vcloudinfo.com/2026/06/tesla-powerwall-home-assistant-watchdog.html). | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `script.notify_live_activity`, `repairs.create` |
| [tesla_model_y.yaml](tesla_model_y.yaml) | Remind the garage and parents to plug in the Model Y after the large garage door closes with a low battery, charging still off, both parents home, and a 24-hour mobile snooze available from the push reminder. | `sensor.spaceship_battery_level`, `switch.spaceship_charge`, `person.carlo`, `person.stacey`, `cover.large_garage_door`, `input_datetime.tesla_model_y_last_garage_close`, `input_datetime.tesla_model_y_plug_in_snooze_until`, `notify.alexa_media_garage`, `script.notify_engine_two_button` |
| [vacuum.yaml](vacuum.yaml) | Dreame vacuum orchestration with room tracking, push alerts, Activity feed, Repairs issues on errors, and Alexa one-off room-clean switches. | `input_select.l10s_vacuum_phase`, `sensor.l10s_vacuum_error`, `repairs.create` |
| [hass_agent_homepc.yaml](hass_agent_homepc.yaml) | Mirrors PC lock/unlock state to the office lamp and wakes `CARLO-HOMEPC` on workday morning bed exits. | `sensor.carlo_homepc_carlo_homepc_sessionstate`, `button.carlo_home`, `switch.office_lamp_switch` |
@@ -110,7 +110,7 @@ When a package has a dedicated blog post or video, I link it right inside the YA
| [holiday.yaml](holiday.yaml) | How the holiday/flag sensor works and drives lighting playlists. | [Blog + video breakdown](https://www.vcloudinfo.com/2019/02/breaking-down-the-flag-sensor-in-home-assistant.html) |
| [lightning.yaml](lightning.yaml) | Blitzortung detector wiring, strike alerts, and snooze workflow. | [Blog](https://www.vcloudinfo.com/2020/08/adding-a-lightning-sensor-to-home-assistant.html) |
| [phynplus.yaml](phynplus.yaml) | Leak-detection response loop with valve state, maintenance guard, Activity Feed context, Repairs tracking, and critical push recovery. | [Video walkthrough](https://youtu.be/xbhgWnomFYI) · [Companion post](https://www.vcloudinfo.com/2026/06/home-assistant-leak-detection-automations.html) · [Original Phyn Plus post](https://www.vcloudinfo.com/2020/05/phyn-plus-smart-water-shutoff-device.html) |
| [powerwall.yaml](powerwall.yaml) | Monitoring Tesla Powerwall health + what to automate when the grid drops. | [Blog](https://www.vcloudinfo.com/2018/01/going-green-to-save-some-green-in-2018.html) |
| [powerwall.yaml](powerwall.yaml) | Monitoring Tesla Powerwall health + what to automate when the grid drops. | [Video walkthrough](https://youtu.be/hR_0lFEE2bA) · [Companion post](https://www.vcloudinfo.com/2026/06/tesla-powerwall-home-assistant-watchdog.html) · [Original solar/Powerwall blog](https://www.vcloudinfo.com/2018/01/going-green-to-save-some-green-in-2018.html) |
| [vacation_mode.yaml](vacation_mode.yaml) | Sustained-away Vacation Mode, house-sitter visit tracking, reminders, missed-visit alerts, secure-house checks, and vacation house digests that keep Powerwall status visible. | [Video walkthrough](https://youtu.be/15kRcFaVV2Y) · [Blog](https://www.vcloudinfo.com/2026/05/home-assistant-vacation-mode-house-sitter-automation.html) |
| [vacuum.yaml](vacuum.yaml) | Dreame away-only cleaning, room queues, sweep/mop phases, Alexa one-off room commands, and rescue notifications. | [Video walkthrough](https://youtu.be/KKOWSKuF5jA) · [Companion post](https://www.vcloudinfo.com/2026/05/home-assistant-vacuum-automations-dreame-2026.html) · [Older Neato post](https://www.vcloudinfo.com/2020/05/home-assistant-neato-vacuum-automation.html) |
| [pihole_ha.yaml](pihole_ha.yaml) | Sync Pi-hole blocking state across HA DNS nodes. | |
@@ -128,7 +128,7 @@ These are the devices that power the packages above. Affiliate links never chang
| Amazon Echo Show | Pops up the front doorbell camera when the August lock unlocks. | [august.yaml](august.yaml) | [![Buy](https://img.shields.io/badge/Buy-Echo%20Show-orange?logo=amazon)](https://amzn.to/4ptA3YO) |
| Phyn Plus water shutoff | [phynplus.yaml](phynplus.yaml) | Leak events trigger valve closes + critical push notifications. [Video walkthrough](https://youtu.be/xbhgWnomFYI) and [companion post](https://www.vcloudinfo.com/2026/06/home-assistant-leak-detection-automations.html). | [![Buy](https://img.shields.io/badge/Buy-Phyn%20Plus-orange?logo=amazon)](https://amzn.to/2Zy3sbJ) |
| Rachio sprinkler controller | [rachio.yaml](rachio.yaml) | Rain skips and seasonal watering adjustments happen automatically. | [![Buy](https://img.shields.io/badge/Buy-Rachio-orange?logo=amazon)](https://amzn.to/2eoPKBW) |
| Tesla Powerwall 2 | [powerwall.yaml](powerwall.yaml) | Grid outages kick off load-shed scripts and status pings. | [![Buy](https://img.shields.io/badge/Buy-Powerwall-orange?logo=tesla)](https://amzn.to/3UM4BZ5) |
| Tesla Powerwall 2 | [powerwall.yaml](powerwall.yaml) | Grid outages kick off status pings, load-shed scripts, and low-charge charging watchdog alerts; see the [video walkthrough](https://youtu.be/hR_0lFEE2bA). | [![Buy](https://img.shields.io/badge/Buy-Powerwall-orange?logo=tesla)](https://amzn.to/3UM4BZ5) |
| Google Nest thermostat | [climate.yaml](climate.yaml) | Presence/weather/grid-aware cooling targets, humidity pulses, and eco recovery. | [![Buy](https://img.shields.io/badge/Buy-Nest%20Thermostat-orange?logo=google)](https://amzn.to/4olpINw) |
| Dreame/Neato vacuum | [vacuum.yaml](vacuum.yaml) | Away-only room queues, sweep/mop phases, Alexa room cleans, rescue notifications, and voice callouts. [Video walkthrough](https://youtu.be/KKOWSKuF5jA) and [companion post](https://www.vcloudinfo.com/2026/05/home-assistant-vacuum-automations-dreame-2026.html). | [![Buy](https://img.shields.io/badge/Buy-Vacuum-orange?logo=amazon)](https://amzn.to/4f7NpFP) |
| NodeMCU motion/contact sensor | [alarm.yaml](alarm.yaml), [office_motion.yaml](office_motion.yaml) | ESP8266 nodes feed the alarm matrix and room-aware lighting. | [![Buy](https://img.shields.io/badge/Buy-Motion%20Node-orange?logo=amazon)](https://amzn.to/2oUgj5i) |
+177 -70
View File
@@ -7,7 +7,8 @@
# Thermostat helpers for upstairs/downstairs comfort.
# -------------------------------------------------------------------
# Related Issue: 1571
# Notes: Filter due alerts include 3-day snooze and Filter Changed push actions.
# Notes: Filter due alerts use a 350h runtime threshold, 9-month hard limit, 3-day snooze, and Filter Changed push actions.
# Notes: Condenser line cleanout uses a single 360-day reminder for both HVAC systems.
# Video: https://youtu.be/y47KSflS1aw
# Blog: https://www.vcloudinfo.com/2026/06/home-assistant-notification-snooze-buttons.html
######################################################################
@@ -53,19 +54,43 @@ template:
- name: "Downstairs AC is Cooling"
unique_id: downstairs_ac_cooling
state: >
{{ state_attr('climate.downstairs', 'hvac_action') == 'cooling' }}
{% set action = state_attr('climate.downstairs', 'hvac_action') %}
{% set current = state_attr('climate.downstairs', 'current_temperature') | float(none) %}
{% set target = state_attr('climate.downstairs', 'temperature') | float(none) %}
{{
action == 'cooling'
or (
action is none
and is_state('climate.downstairs', 'cool')
and current is not none
and target is not none
and current > target
)
}}
- name: "Upstairs AC is Cooling"
unique_id: upstairs_ac_cooling
state: >
{{ state_attr('climate.upstairs', 'hvac_action') == 'cooling' }}
{% set action = state_attr('climate.upstairs', 'hvac_action') %}
{% set current = state_attr('climate.upstairs', 'current_temperature') | float(none) %}
{% set target = state_attr('climate.upstairs', 'temperature') | float(none) %}
{{
action == 'cooling'
or (
action is none
and is_state('climate.upstairs', 'cool')
and current is not none
and target is not none
and current > target
)
}}
sensor:
- name: "Downstairs AC Cooling Numeric"
unique_id: downstairs_ac_cooling_numeric
state: "{{ 1 if is_state('binary_sensor.downstairs_ac_cooling', 'on') else 0 }}"
state: "{{ 1 if is_state('binary_sensor.downstairs_ac_is_cooling', 'on') else 0 }}"
- name: "Upstairs AC Cooling Numeric"
unique_id: upstairs_ac_cooling_numeric
state: "{{ 1 if is_state('binary_sensor.upstairs_ac_cooling', 'on') else 0 }}"
state: "{{ 1 if is_state('binary_sensor.upstairs_ac_is_cooling', 'on') else 0 }}"
input_datetime:
downstairs_last_filter_change:
@@ -84,6 +109,16 @@ input_datetime:
name: Upstairs Filter Snooze Until
has_date: true
has_time: true
hvac_condenser_lines_last_cleaned:
name: HVAC Condenser Lines Last Cleaned
has_date: true
has_time: false
initial: "2026-06-28"
hvac_condenser_lines_cleaning_snooze_until:
name: HVAC Condenser Lines Cleaning Snooze Until
has_date: true
has_time: true
initial: "2026-06-28 00:00:00"
# ---------------------------------------------------------------------------
# Integration sensors tally runtime based on compressor state
@@ -144,6 +179,19 @@ script:
target:
entity_id: sensor.upstairs_ac_runtime_since_last_filter_change
reset_hvac_condenser_lines_cleaned:
alias: Reset HVAC Condenser Lines Cleaned
mode: queued
sequence:
- service: input_datetime.set_datetime
data:
entity_id: input_datetime.hvac_condenser_lines_last_cleaned
date: "{{ now().strftime('%Y-%m-%d') }}"
- service: input_datetime.set_datetime
data:
entity_id: input_datetime.hvac_condenser_lines_cleaning_snooze_until
datetime: "{{ (now() - timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') }}"
set_downstairs_target_temp_based_on_conditions:
alias: Set Downstairs Target Temperature Based on Conditions
mode: single
@@ -285,81 +333,76 @@ script:
### There are also some automations in the POWERWALL.yaml package when the grid is down.
##############################################################################
automation:
- alias: Notify Downstairs Filter Change Due
description: Notify when downstairs runtime exceeds threshold since last filter change
- alias: Notify Climate Filter Change Due
description: Notify when HVAC filter runtime exceeds 350h or age reaches 9 months
trigger:
- platform: numeric_state
entity_id: sensor.downstairs_ac_runtime_since_last_filter_change
above: 800 # hours
above: 350
id: downstairs_runtime
- platform: time
at: "09:00:00"
id: downstairs_daily
- platform: numeric_state
entity_id: sensor.upstairs_ac_runtime_since_last_filter_change
above: 350
id: upstairs_runtime
- platform: time
at: "09:10:00"
id: upstairs_daily
variables:
location: "{{ 'upstairs' if trigger.id.startswith('upstairs') else 'downstairs' }}"
location_title: "{{ 'Upstairs' if location == 'upstairs' else 'Downstairs' }}"
runtime_sensor: "sensor.{{ location }}_ac_runtime_since_last_filter_change"
last_filter_entity: "input_datetime.{{ location }}_last_filter_change"
snooze_entity: "input_datetime.{{ location }}_filter_snooze_until"
runtime_hours: "{{ states(runtime_sensor) | float(0) }}"
last_filter_ts: "{{ as_timestamp(states(last_filter_entity), 0) }}"
filter_age_days: "{{ ((as_timestamp(now()) - (last_filter_ts | float(0))) / 86400) | round(0) }}"
due_reason: >-
{% set runtime_due = runtime_hours | float(0) > 350 %}
{% set age_due = (last_filter_ts | float(0)) == 0 or (filter_age_days | int(0)) >= 274 %}
{% if runtime_due and age_due %}
Runtime has exceeded 350h and the filter is {{ filter_age_days }} days old.
{% elif runtime_due %}
Runtime has exceeded 350h.
{% else %}
Filter age has reached the 9-month hard limit ({{ filter_age_days }} days).
{% endif %}
condition:
- condition: numeric_state
entity_id: sensor.downstairs_ac_runtime_since_last_filter_change
above: 800
- condition: template
value_template: >-
{% set snooze_until = as_timestamp(states('input_datetime.downstairs_filter_snooze_until'), 0) %}
{{
(runtime_hours | float(0) > 350)
or (last_filter_ts | float(0)) == 0
or (filter_age_days | int(0)) >= 274
}}
- condition: template
value_template: >-
{% set snooze_until = as_timestamp(states(snooze_entity), 0) %}
{{ snooze_until <= as_timestamp(now()) }}
action:
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: >-
Downstairs AC filter due (runtime >800h). Last changed {{ ((now() - states.input_datetime.downstairs_last_filter_change.last_changed).total_seconds() / 86400) | round(0) }} days ago.
{{ location_title }} AC filter due. {{ due_reason }} Runtime is {{ runtime_hours | round(1) }}h.
- service: script.notify_engine_two_button
data:
title: "Home Maintenance Reminder"
value1: "It's time to change your Downstairs AC filter."
value1: "It's time to change your {{ location_title }} AC filter."
value2: >
Runtime has exceeded 800h. Last changed {{ ((now() - states.input_datetime.downstairs_last_filter_change.last_changed).total_seconds() / 86400) | round(0) }} days ago.
{{ due_reason }} Runtime is {{ runtime_hours | round(1) }}h.
title1: "Snooze 3d"
action1: "SNOOZE_DOWNSTAIRS_FILTER_3D"
action1: "{{ 'SNOOZE_UPSTAIRS_FILTER_3D' if location == 'upstairs' else 'SNOOZE_DOWNSTAIRS_FILTER_3D' }}"
icon1: "sfsymbols:clock"
title2: "Filter Changed"
action2: "RESET_DOWNSTAIRS_FILTER"
action2: "{{ 'RESET_UPSTAIRS_FILTER' if location == 'upstairs' else 'RESET_DOWNSTAIRS_FILTER' }}"
icon2: "sfsymbols:checkmark.circle"
who: "carlo"
group: "maintenance"
level: "active"
- alias: Notify Upstairs Filter Change Due
description: Notify when upstairs runtime exceeds threshold since last filter change
trigger:
- platform: numeric_state
entity_id: sensor.upstairs_ac_runtime_since_last_filter_change
above: 450 # hours
- platform: time
at: "09:10:00"
condition:
- condition: numeric_state
entity_id: sensor.upstairs_ac_runtime_since_last_filter_change
above: 450
- condition: template
value_template: >-
{% set snooze_until = as_timestamp(states('input_datetime.upstairs_filter_snooze_until'), 0) %}
{{ snooze_until <= as_timestamp(now()) }}
action:
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: >-
Upstairs AC filter due (runtime >450h). Last changed {{ ((now() - states.input_datetime.upstairs_last_filter_change.last_changed).total_seconds() / 86400) | round(0) }} days ago.
- service: script.notify_engine_two_button
data:
title: "Home Maintenance Reminder"
value1: "It's time to change your Upstairs AC filter."
value2: >
Runtime has exceeded 450h. Last changed {{ ((now() - states.input_datetime.upstairs_last_filter_change.last_changed).total_seconds() / 86400) | round(0) }} days ago.
title1: "Snooze 3d"
action1: "SNOOZE_UPSTAIRS_FILTER_3D"
icon1: "sfsymbols:clock"
title2: "Filter Changed"
action2: "RESET_UPSTAIRS_FILTER"
icon2: "sfsymbols:checkmark.circle"
who: "carlo"
group: "maintenance"
- alias: Climate Filter Reminder Actions
id: 6d7056d0-90ce-4c4f-b8b1-fd32a7e58311
mode: queued
@@ -384,48 +427,112 @@ automation:
event_data:
action: RESET_UPSTAIRS_FILTER
id: upstairs_reset
variables:
location: "{{ 'upstairs' if trigger.id.startswith('upstairs') else 'downstairs' }}"
location_title: "{{ 'Upstairs' if location == 'upstairs' else 'Downstairs' }}"
snooze_entity: "input_datetime.{{ location }}_filter_snooze_until"
reset_script: "script.reset_{{ location }}_filter"
action:
- choose:
- conditions: "{{ trigger.id == 'downstairs_snooze' }}"
- conditions: "{{ trigger.id.endswith('_snooze') }}"
sequence:
- variables:
snooze_until: "{{ (now() + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S') }}"
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.downstairs_filter_snooze_until
entity_id: "{{ snooze_entity }}"
data:
datetime: "{{ snooze_until }}"
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "Downstairs AC filter reminder snoozed until {{ snooze_until }}."
- conditions: "{{ trigger.id == 'upstairs_snooze' }}"
message: "{{ location_title }} AC filter reminder snoozed until {{ snooze_until }}."
- conditions: "{{ trigger.id.endswith('_reset') }}"
sequence:
- service: "{{ reset_script }}"
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "{{ location_title }} AC filter reset from notification action."
- alias: Notify HVAC Condenser Lines Cleaning Due
id: 4dafac96-3163-4d63-847a-76727005f75b
description: Notify every 360 days when the outdoor condenser lines should be cleaned
trigger:
- platform: time
at: "09:20:00"
variables:
cleaned: "{{ states('input_datetime.hvac_condenser_lines_last_cleaned') }}"
cleaned_ts: >-
{% if cleaned in ['unknown', 'unavailable', 'none', ''] %}
0
{% else %}
{{ as_timestamp(strptime(cleaned, '%Y-%m-%d'), 0) }}
{% endif %}
days_since_cleaned: "{{ ((as_timestamp(now()) - (cleaned_ts | float(0))) / 86400) | round(0) }}"
condition:
- condition: template
value_template: "{{ (cleaned_ts | float(0)) == 0 or (days_since_cleaned | int(0)) >= 360 }}"
- condition: template
value_template: >-
{% set snooze_until = as_timestamp(states('input_datetime.hvac_condenser_lines_cleaning_snooze_until'), 0) %}
{{ snooze_until <= as_timestamp(now()) }}
action:
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "HVAC condenser lines cleanout due; last cleaned {{ days_since_cleaned }} days ago."
- service: script.notify_engine_two_button
data:
title: "Home Maintenance Reminder"
value1: "Clean the HVAC condenser lines."
value2: "360-day cleanout is due; last cleaned {{ days_since_cleaned }} days ago."
title1: "Snooze 3d"
action1: "SNOOZE_HVAC_CONDENSER_LINES_3D"
icon1: "sfsymbols:clock"
title2: "Cleaned"
action2: "RESET_HVAC_CONDENSER_LINES_CLEANED"
icon2: "sfsymbols:checkmark.circle"
who: "carlo"
group: "maintenance"
level: "active"
- alias: Climate Condenser Lines Reminder Actions
id: dfe89869-4bd3-47f6-bf0d-612bf7ee949f
mode: queued
trigger:
- platform: event
event_type: mobile_app_notification_action
event_data:
action: SNOOZE_HVAC_CONDENSER_LINES_3D
id: snooze
- platform: event
event_type: mobile_app_notification_action
event_data:
action: RESET_HVAC_CONDENSER_LINES_CLEANED
id: reset
action:
- choose:
- conditions: "{{ trigger.id == 'snooze' }}"
sequence:
- variables:
snooze_until: "{{ (now() + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S') }}"
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.upstairs_filter_snooze_until
entity_id: input_datetime.hvac_condenser_lines_cleaning_snooze_until
data:
datetime: "{{ snooze_until }}"
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "Upstairs AC filter reminder snoozed until {{ snooze_until }}."
- conditions: "{{ trigger.id == 'downstairs_reset' }}"
message: "HVAC condenser lines cleanout reminder snoozed until {{ snooze_until }}."
- conditions: "{{ trigger.id == 'reset' }}"
sequence:
- service: script.reset_downstairs_filter
- service: script.reset_hvac_condenser_lines_cleaned
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "Downstairs AC filter reset from notification action."
- conditions: "{{ trigger.id == 'upstairs_reset' }}"
sequence:
- service: script.reset_upstairs_filter
- service: script.send_to_logbook
data:
topic: "MAINTENANCE"
message: "Upstairs AC filter reset from notification action."
message: "HVAC condenser lines cleanout reset from notification action."
- alias: 'AC Status Announcement'
id: 7812fdaf-a3f8-498b-8f07-28e977e528fe
+120 -10
View File
@@ -3,9 +3,11 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Holiday Package - Flag/holiday sensors and lighting triggers - Holiday routines, notifications, and lighting tweaks.
# Centralizes the Holiday Package - Flag/holiday sensors and lighting triggers package configuration and helpers.
# Holiday Package - Flag/holiday sensors and lighting mode
# 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.
######################################################################
# Video breakdown: https://www.vcloudinfo.com/2019/02/breaking-down-the-flag-sensor-in-home-assistant.html
# Modified for my own fun stuff!
@@ -26,7 +28,7 @@ homeassistant:
# 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
# it checks in the dynamic section. If neither exists, the value will be empty.
###############################################################################
sensor:
- platform: rest
@@ -34,13 +36,21 @@ sensor:
name: Holiday
scan_interval: 14400
value_template: >
{% set today = now().month ~ '/' ~ now().day %}
{% set holiday = value_json.MAJOR_US.static[today] if today in value_json.MAJOR_US.static else "" %}
{% if holiday | trim == "" %}
{% set today = now().month ~ '/' ~ now().day ~ '/' ~ now().year %}
{% set holiday = value_json.MAJOR_US.dynamic[today] if today in value_json.MAJOR_US.dynamic else "" %}
{% 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 %}
{{ holiday }}
json_attributes:
- MAJOR_US
################################################################################
# Sensor Uses Flag data generated by AI
@@ -52,16 +62,19 @@ sensor:
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 %}
{% 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
@@ -129,3 +142,100 @@ sensor:
value_template: "{{ (value|replace(' days', '')) | int }}"
unit_of_measurement: Days
scan_interval: 43200
template:
- sensor:
- name: Holiday Lighting Mode
unique_id: holiday_lighting_mode
icon: mdi:string-lights
state: >-
{%- set today = today_at('00:00') -%}
{%- 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 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 -%}
{%- 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 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 -%}
{%- 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 -%}
{%- set mode.value = 'mothers_day' -%}
{%- elif days_to.fathers < 4 -%}
{%- set mode.value = 'fathers_day' -%}
{%- elif days_to.memorial < 3 -%}
{%- set mode.value = 'RWB' -%}
{%- elif mmdd == 704 -%}
{%- set mode.value = 'RWB' -%}
{%- elif days_to.labor < 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 -%}
{%- set mode.value = 'thanksgiving' -%}
{%- elif states('sensor.chanukkah_countdown') | int(9999) <= 1 -%}
{%- set mode.value = 'hanukkah' -%}
{%- elif christmas_days >= 0 and christmas_days <= 25 -%}
{%- set mode.value = 'christmas' -%}
{%- elif mmdd == 1231 -%}
{%- set mode.value = 'new_years_day' -%}
{%- endif -%}
{{- mode.value -}}
- name: Holiday Lighting Scene
unique_id: holiday_lighting_scene
icon: mdi:palette
state: >-
{%- set mode = states('sensor.holiday_lighting_mode') | trim -%}
{%- if mode in ['unknown', 'unavailable', 'none', ''] -%}
scene.month_standard_colors
{%- else -%}
scene.month_{{ mode }}_colors
{%- endif -%}
attributes:
mode: "{{ states('sensor.holiday_lighting_mode') }}"
holiday: "{{ states('sensor.holiday') }}"
flag_day: "{{ states('sensor.flag') }}"
source: config/www/json_data holiday and flag calendars
+18 -18
View File
@@ -37,27 +37,27 @@ Reusable lighting and ambiance presets. Automations and scripts call these scene
| Red_living_Room | All fixtures red, mid/high brightness | Alert/entry automations (garage/doors) |
| Living_Room_Daytime_Cool | 5500K cool white, full brightness | Living room default automation (day) |
| Living_Room_Evening_Amber | 2700K warm/amber, softer brightness | Living room default automation (night) |
| month_standard_colors | Baseline white/neutral monthly palette | `script.monthly_color_scene` after sunset |
| month_RWB_colors | Red/white/blue set (patriotic/July 4th) | `script.monthly_color_scene` (flag/holiday) |
| month_valentine_colors | Valentine pinks/reds | `script.monthly_color_scene` (Feb 1014) |
| month_mardi_gras_colors | Purple/green/gold Mardi Gras | `script.monthly_color_scene` (Mar 5) |
| month_st_patty_colors | Green-centric St. Patrick's | `script.monthly_color_scene` (Mar 1517) |
| month_pi_colors | Pi Day playful hues | `script.monthly_color_scene` (Mar 14) |
| month_easter_colors | Pastel Easter set | `script.monthly_color_scene` (Easter countdown) |
| month_starwars_colors | Star Wars themed mix | `script.monthly_color_scene` (May 4) |
| month_cinco_de_mayo_colors | Cinco de Mayo festive mix | `script.monthly_color_scene` (May 5) |
| month_mothers_day_colors | Mother's Day palette | `script.monthly_color_scene` (countdown) |
| month_fathers_day_colors | Father's Day palette | `script.monthly_color_scene` (countdown) |
| month_halloween_colors | Halloween oranges/purples | `script.monthly_color_scene` (Oct 131) |
| month_veterans_colors | Veterans Day palette | `script.monthly_color_scene` (Nov 11) |
| month_thanksgiving_colors | Autumn harvest tones | `script.monthly_color_scene` (countdown) |
| month_hanukkah_colors | Hanukkah blues/whites | `script.monthly_color_scene` (Hanukkah countdown) |
| month_christmas_colors | Christmas reds/greens | `script.monthly_color_scene` (Christmas countdown) |
| month_new_years_day_colors | New Year's bright/celebratory | `script.monthly_color_scene` (Jan 1 & Dec 31) |
| month_standard_colors | Baseline white/neutral monthly palette | `sensor.holiday_lighting_mode` = `standard` |
| month_RWB_colors | Red/white/blue set (patriotic/July 4th) | `sensor.holiday_lighting_mode` = `RWB` |
| month_valentine_colors | Valentine pinks/reds | `sensor.holiday_lighting_mode` = `valentine` |
| month_mardi_gras_colors | Purple/green/gold Mardi Gras | `sensor.holiday_lighting_mode` = `mardi_gras` |
| month_st_patty_colors | Green-centric St. Patrick's | `sensor.holiday_lighting_mode` = `st_patty` |
| month_pi_colors | Pi Day playful hues | `sensor.holiday_lighting_mode` = `pi` |
| month_easter_colors | Pastel Easter set | `sensor.holiday_lighting_mode` = `easter` |
| month_starwars_colors | Star Wars themed mix | `sensor.holiday_lighting_mode` = `starwars` |
| month_cinco_de_mayo_colors | Cinco de Mayo festive mix | `sensor.holiday_lighting_mode` = `cinco_de_mayo` |
| month_mothers_day_colors | Mother's Day palette | `sensor.holiday_lighting_mode` = `mothers_day` |
| month_fathers_day_colors | Father's Day palette | `sensor.holiday_lighting_mode` = `fathers_day` |
| month_halloween_colors | Halloween oranges/purples | `sensor.holiday_lighting_mode` = `halloween` |
| month_veterans_colors | Veterans Day palette | `sensor.holiday_lighting_mode` = `veterans` |
| month_thanksgiving_colors | Autumn harvest tones | `sensor.holiday_lighting_mode` = `thanksgiving` |
| month_hanukkah_colors | Hanukkah blues/whites | `sensor.holiday_lighting_mode` = `hanukkah` |
| month_christmas_colors | Christmas reds/greens | `sensor.holiday_lighting_mode` = `christmas` |
| month_new_years_day_colors | New Year's bright/celebratory | `sensor.holiday_lighting_mode` = `new_years_day` |
### Tips
- Adjust scenes once and let all dependent automations inherit the change.
- Pair with `script/monthly_color_scene.yaml` for dynamic monthly palettes.
- Pair with `script/monthly_color_scene.yaml`; the active mode and scene are exposed by `sensor.holiday_lighting_mode` and `sensor.holiday_lighting_scene`.
**All of my configuration files are tested against the most stable version of home-assistant.**
+1 -1
View File
@@ -32,7 +32,7 @@ Reusable scripts that other automations call for notifications, lighting, safety
| [send_to_logbook.yaml](send_to_logbook.yaml) | Generic `logbook.log` helper for Activity feed entries (Issue #1550). |
| [joanna_dispatch.yaml](joanna_dispatch.yaml) | Shared AGENT engineer dispatch contract that routes HA-detected issues into Joanna/BearClaw remediation. |
| [speech_engine.yaml](speech_engine.yaml) | TTS/announcement orchestration with templated speech; speech processing can bypass LLM rewriting for exact messages and also routes garage/office Echo announcements. |
| [monthly_color_scene.yaml](monthly_color_scene.yaml) | Seasonal lighting scenes used across automations. |
| [monthly_color_scene.yaml](monthly_color_scene.yaml) | Seasonal lighting dispatcher that follows `sensor.holiday_lighting_scene`. |
| [interior_off.yaml](interior_off.yaml) | One-call "all interior lights off" helper. |
### Joanna + BearClaw AGENT engineer handoff
+7 -50
View File
@@ -3,8 +3,9 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Youtube Video description of how I use this script - Example action call was documented in the legacy header.
# Defines the Youtube Video description of how I use this script sequence for reuse by automations and dashboards.
# Monthly Color Scene - Exterior seasonal lighting scene dispatcher.
# Related Issue: 1774
# Turns on the scene exposed by sensor.holiday_lighting_scene after dark.
# -------------------------------------------------------------------
# Notes: https://www.vcloudinfo.com/2018/10/easy-smart-home-gadgets-i-use-for-my.html
# Notes: https://www.vcloudinfo.com/2017/08/diy-outdoor-smart-home-led-strips.html
@@ -13,6 +14,7 @@
# Notes: - service: script.monthly_color_scene
# Notes: scenes should be named month_[01-12]_colors (month_06_colors)
# Notes: Color help - http://www.esbnyc.com/explore/tower-lights/calendar
# Notes: Active mode is inspectable at sensor.holiday_lighting_mode.
######################################################################
monthly_color_scene:
sequence:
@@ -22,51 +24,6 @@ monthly_color_scene:
- service: scene.turn_on
data:
entity_id: >
scene.month_
{%- if states.sensor.flag.state == "True" -%}
RWB
{%- elif now().strftime("%m%d")|int == 101 -%}
new_years_day
{%- elif now().strftime("%m%d")|int >= 210
and now().strftime("%m%d")|int <= 214-%}
valentine
{%- elif now().strftime("%m%d")|int == 305 -%}
mardi_gras
{%- elif now().strftime("%m%d")|int == 314 -%}
pi
{%- elif now().strftime("%m%d")|int >= 315
and now().strftime("%m%d")|int <= 317-%}
st_patty
{%- elif states('sensor.easter_countdown') | int < 4 -%}
easter
{%- elif now().strftime("%m%d")|int == 504 -%}
starwars
{%- elif now().strftime("%m%d")|int == 505 -%}
cinco_de_mayo
{%- elif states('sensor.mothers_countdown') | int < 4 -%}
mothers_day
{%- elif states('sensor.fathers_countdown') | int < 4 -%}
fathers_day
{%- elif states('sensor.memorial_day_countdown') | int < 3 -%}
RWB
{%- elif now().strftime("%m%d")|int == 704 -%}
RWB
{%- elif states('sensor.labor_day_countdown') | int < 3 -%}
RWB
{%- elif now().strftime("%m%d")|int >= 1001
and now().strftime("%m%d")|int <= 1031-%}
halloween
{%- elif now().strftime("%m%d")|int == 1111 -%}
veterans
{%- elif states('sensor.thanksgiving_day_countdown') | int < 4 -%}
thanksgiving
{%- elif states('sensor.chanukkah_countdown') | int <= 1 -%}
hanukkah
{%- elif states('sensor.christmas_countdown') | int <= 25 -%}
christmas
{%- elif now().strftime("%m%d")|int == 1231 -%}
new_years_day
{%- else -%}
standard
{%- endif -%}_colors
entity_id: >-
{%- set scene = states('sensor.holiday_lighting_scene') | trim -%}
{{ scene if scene[:12] == 'scene.month_' else 'scene.month_standard_colors' }}
+161
View File
@@ -0,0 +1,161 @@
param(
[int]$Months = 24,
[datetime]$StartDate = (Get-Date).Date
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$holidayPath = Join-Path $repoRoot 'config/www/json_data/holidays.json'
$flagPath = Join-Path $repoRoot 'config/www/json_data/flag_days.json'
$scenePath = Join-Path $repoRoot 'config/scene/monthly_colors.yaml'
$endDate = $StartDate.Date.AddMonths($Months)
$errors = @()
function Read-JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Missing JSON file: $Path"
}
try {
return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json -AsHashtable
} catch {
throw "Invalid JSON in $Path`: $($_.Exception.Message)"
}
}
function Parse-DateKey {
param(
[Parameter(Mandatory = $true)]
[string]$DateText
)
return [datetime]::Parse(
$DateText,
[Globalization.CultureInfo]::InvariantCulture
).Date
}
function Get-DynamicEvents {
param(
[Parameter(Mandatory = $true)]
[object]$DynamicMap,
[Parameter(Mandatory = $true)]
[string[]]$Names
)
$events = @()
foreach ($dateText in $DynamicMap.Keys) {
$eventDate = Parse-DateKey -DateText ([string]$dateText)
$eventName = [string]$DynamicMap[$dateText]
if ($eventDate -ge $StartDate.Date -and $eventDate -lt $endDate -and $Names -contains $eventName) {
$events += [pscustomobject]@{
Name = $eventName
Date = $eventDate
}
}
}
return $events | Sort-Object Date, Name
}
$holidayJson = Read-JsonFile -Path $holidayPath
$flagJson = Read-JsonFile -Path $flagPath
$holidayData = $holidayJson['MAJOR_US']
$flagData = $flagJson['Flag_Days_US']
$holidayStatic = $holidayData['static']
$holidayDynamic = $holidayData['dynamic']
$flagDynamic = $flagData['dynamic']
$sceneText = Get-Content -Raw -LiteralPath $scenePath
$sceneNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
foreach ($match in [regex]::Matches($sceneText, '(?m)^\s*-\s+name:\s+(month_[A-Za-z0-9_]+_colors)\s*$')) {
[void]$sceneNames.Add($match.Groups[1].Value)
}
$lightingModes = @(
'standard',
'RWB',
'new_years_day',
'valentine',
'mardi_gras',
'pi',
'st_patty',
'easter',
'starwars',
'cinco_de_mayo',
'mothers_day',
'fathers_day',
'halloween',
'veterans',
'thanksgiving',
'hanukkah',
'christmas'
)
foreach ($mode in $lightingModes) {
$sceneName = "month_${mode}_colors"
if (-not $sceneNames.Contains($sceneName)) {
$errors += "Missing scene for lighting mode '$mode': expected $sceneName in $scenePath"
}
}
$staticRequirements = @(
@{ Key = '1/1'; Name = 'New Years Day' },
@{ Key = '2/14'; Name = 'Valentines Day' },
@{ Key = '3/14'; Name = 'Pi Day' },
@{ Key = '3/17'; Name = 'St. Patricks Day' },
@{ Key = '5/4'; Name = 'Star Wars Day' },
@{ Key = '5/5'; Name = 'Cinco de Mayo' },
@{ Key = '7/4'; Name = 'Independence Day' },
@{ Key = '10/31'; Name = 'Halloween' },
@{ Key = '11/11'; Name = 'Veterans Day' },
@{ Key = '12/25'; Name = 'Christmas Day' },
@{ Key = '12/31'; Name = 'New Years Eve' }
)
foreach ($requirement in $staticRequirements) {
if (-not $holidayStatic.ContainsKey($requirement.Key)) {
$errors += "Missing static holiday key $($requirement.Key) for $($requirement.Name)"
}
}
$dynamicRequirements = @(
@{ Name = 'Easter Sunday'; Source = 'holidays'; Map = $holidayDynamic },
@{ Name = 'Mothers Day'; Source = 'holidays'; Map = $holidayDynamic },
@{ Name = 'Fathers Day'; Source = 'holidays'; Map = $holidayDynamic },
@{ Name = 'Thanksgiving Day'; Source = 'holidays'; Map = $holidayDynamic },
@{ Name = 'Memorial Day'; Source = 'flag_days'; Map = $flagDynamic },
@{ Name = 'Labor Day'; Source = 'flag_days'; Map = $flagDynamic }
)
foreach ($requirement in $dynamicRequirements) {
$events = @(Get-DynamicEvents -DynamicMap $requirement.Map -Names @($requirement.Name))
if ($events.Count -eq 0) {
$errors += "No $($requirement.Source) calendar coverage for '$($requirement.Name)' between $($StartDate.ToString('yyyy-MM-dd')) and $($endDate.ToString('yyyy-MM-dd'))"
}
}
Write-Host "Holiday lighting coverage window: $($StartDate.ToString('yyyy-MM-dd')) to $($endDate.ToString('yyyy-MM-dd'))"
Write-Host "JSON: OK - parsed holidays.json and flag_days.json"
Write-Host "Scenes: $($sceneNames.Count) monthly scenes found"
if ($errors.Count -gt 0) {
foreach ($err in $errors) {
Write-Host "ERROR: $err"
}
exit 1
}
foreach ($requirement in $dynamicRequirements) {
$events = @(Get-DynamicEvents -DynamicMap $requirement.Map -Names @($requirement.Name))
$dates = ($events | ForEach-Object { $_.Date.ToString('yyyy-MM-dd') }) -join ', '
Write-Host "$($requirement.Name): $dates"
}
Write-Host "Coverage OK: required lighting scenes and dynamic holiday dates are present for the requested window."