diff --git a/config/dashboards/overview/partials/water_sections.yaml b/config/dashboards/overview/partials/water_sections.yaml index daef02e9..deefd5bc 100644 --- a/config/dashboards/overview/partials/water_sections.yaml +++ b/config/dashboards/overview/partials/water_sections.yaml @@ -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: diff --git a/config/packages/README.md b/config/packages/README.md index 9fb75687..ef5ddca3 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -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) | diff --git a/config/packages/climate.yaml b/config/packages/climate.yaml index 5af8781f..c64ee0fb 100644 --- a/config/packages/climate.yaml +++ b/config/packages/climate.yaml @@ -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 diff --git a/config/packages/holiday.yaml b/config/packages/holiday.yaml index 79f7c0cd..ea4f01da 100755 --- a/config/packages/holiday.yaml +++ b/config/packages/holiday.yaml @@ -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 diff --git a/config/scene/README.md b/config/scene/README.md index dcb2e833..637bf7dd 100755 --- a/config/scene/README.md +++ b/config/scene/README.md @@ -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 10–14) | -| 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 15–17) | -| 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 1–31) | -| 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.** diff --git a/config/script/README.md b/config/script/README.md index b2c56c41..ee71173f 100755 --- a/config/script/README.md +++ b/config/script/README.md @@ -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 diff --git a/config/script/monthly_color_scene.yaml b/config/script/monthly_color_scene.yaml index 205193a4..93782a44 100755 --- a/config/script/monthly_color_scene.yaml +++ b/config/script/monthly_color_scene.yaml @@ -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' }} diff --git a/tools/holiday_lighting_coverage.ps1 b/tools/holiday_lighting_coverage.ps1 new file mode 100644 index 00000000..929af757 --- /dev/null +++ b/tools/holiday_lighting_coverage.ps1 @@ -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."