diff --git a/config/automation/dark_rainy_day.yaml b/config/automation/dark_rainy_day.yaml index 038554b0..312bdd31 100755 --- a/config/automation/dark_rainy_day.yaml +++ b/config/automation/dark_rainy_day.yaml @@ -1,5 +1,13 @@ ###################################################################### -## Dark House Little extra light - DARK and Cloudy or just rainy. +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Dark House Little Extra Light - Daytime rain/lightning lighting helper +# Turns on living-room lights and announces weather-related reasons. +# ------------------------------------------------------------------- +# Notes: +# Avoid trigger entity string-splitting; map trigger IDs safely. ###################################################################### - alias: 'Dark House Little extra light' @@ -60,7 +68,16 @@ - service: script.speech_engine data: - value1: "Because of the {{trigger.entity_id.split('_')[2]|replace('precip','rain')|replace('counter','lightning')|replace('carlo','rain') }} {{trigger.entity_id.split('_')[3]|replace('intensity',' ')| replace('carlo','and clouds')}} outside. I will turn on some extra lights in the living room." + value1: >- + {% set reason_map = { + 'sensor.pirateweather_precip': 'rain', + 'sensor.pirateweather_precip_intensity': 'heavy rain', + 'sensor.blitzortung_lightning_counter': 'lightning', + 'group.family': 'rain and clouds', + 'binary_sensor.sleepnumber_carlo_carlo_is_in_bed': 'rain and clouds' + } %} + {% set reason = reason_map.get(trigger.entity_id | default(''), 'weather changes') %} + Because of the {{ reason }} outside. I will turn on some extra lights in the living room. call_outside_weather: 1 call_window_check: 1 call_garage_check: 1 diff --git a/config/packages/README.md b/config/packages/README.md index 0bac7eb3..f68b2011 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -45,7 +45,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [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` | -| [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching telemetry + container/stack Repairs automation and weekly scheduled prune actions across docker_10/14/17/69. | `sensor.docker_*_apt_status`, `binary_sensor.*_stack_status`, `sensor.docker_stacks_down_count`, `repairs.create` | +| [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching telemetry + container/stack Repairs automation, 20-minute Joanna escalation for persistent container outages, and weekly scheduled prune actions across docker_10/14/17/69. | `sensor.docker_*_apt_status`, `binary_sensor.*_stack_status`, `sensor.docker_stacks_down_count`, `repairs.create`, `script.joanna_dispatch` | | [proxmox.yaml](proxmox.yaml) | Proxmox runtime and disk pressure monitoring with Repairs for node degradations plus nightly Frigate reboot. | `binary_sensor.proxmox*_runtime_healthy`, `sensor.proxmox*_disk_used_percentage`, `repairs.create`, `button.qemu_docker2_101_reboot` | | [infrastructure_observability.yaml](infrastructure_observability.yaml) | Normalized WAN/DNS/backup/domain/cert health + website uptime/latency SLO signals for Infrastructure dashboards. | `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `binary_sensor.infra_*` | | [onenote_indexer.yaml](onenote_indexer.yaml) | OneNote indexer health/status monitoring for Joanna, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful` | diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index ed38e002..edc62db0 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -11,7 +11,7 @@ # Notes: Reboots are handled directly on each host by apt_weekly.sh. # Notes: Reboot staggering: docker_14 first, docker_69 second, docker_10 third. # Notes: Container monitoring is dynamic with binary_sensor status preferred over switch state. -# Notes: Includes Portainer stack status repairs and scheduled image prune. +# Notes: Includes Portainer stack status repairs, 20-minute Joanna dispatch for persistent container outages, and scheduled image prune. ###################################################################### input_datetime: @@ -257,7 +257,7 @@ template: icon: mdi:docker state: >- {% set ns = namespace(keys=[], down=[]) %} - {% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) %} + {% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list %} {% set telemetry_degraded = is_state('binary_sensor.docker_container_telemetry_degraded', 'on') %} {% for switch_entity in monitored %} {% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} @@ -297,19 +297,58 @@ template: {% set ns.down = ns.down + [key] %} {% endif %} {% endfor %} - {{ ns.down | sort | join(', ') if (ns.down | count > 0) else 'none' }} + {{ 'ok' if (ns.down | count == 0) else 'down' }} + attributes: + down_containers: >- + {% set ns = namespace(keys=[], down=[]) %} + {% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list %} + {% set telemetry_degraded = is_state('binary_sensor.docker_container_telemetry_degraded', 'on') %} + {% for switch_entity in monitored %} + {% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} + {% if key not in ns.keys %} + {% set ns.keys = ns.keys + [key] %} + {% endif %} + {% endfor %} + {% for key in ns.keys | sort %} + {% set status_entity = 'binary_sensor.' ~ key ~ '_status' %} + {% set status_entity_alt = status_entity ~ '_2' %} + {% set state_entity = 'sensor.' ~ key ~ '_state' %} + {% set state_entity_alt = state_entity ~ '_2' %} + {% set switch_entity = 'switch.' ~ key ~ '_container' %} + {% set switch_entity_alt = switch_entity ~ '_2' %} + {% set resolver = namespace(state='unknown', chosen=false) %} + {% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %} + {% if not resolver.chosen and expand(candidate) | count > 0 %} + {% set candidate_state = states(candidate) | lower %} + {% if candidate_state not in ['unknown', 'unavailable', ''] %} + {% set resolver.state = candidate_state %} + {% set resolver.chosen = true %} + {% endif %} + {% endif %} + {% endfor %} + {% if not resolver.chosen %} + {% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %} + {% if not resolver.chosen and expand(candidate) | count > 0 %} + {% set resolver.state = states(candidate) | lower %} + {% set resolver.chosen = true %} + {% endif %} + {% endfor %} + {% endif %} + {% set effective_state = resolver.state %} + {% if effective_state in ['off', 'stopped'] %} + {% set ns.down = ns.down + [key] %} + {% elif not telemetry_degraded and effective_state in ['unknown', 'unavailable'] %} + {% set ns.down = ns.down + [key] %} + {% endif %} + {% endfor %} + {{ ns.down | sort }} - name: "Docker Containers Down Count" unique_id: docker_containers_down_count icon: mdi:counter state: >- - {% set down_list = states('sensor.docker_containers_down_list') %} - {% set normalized = down_list | lower %} - {% if normalized in ['unknown', 'unavailable', 'none', ''] %} - 0 - {% else %} - {{ down_list.split(',') | map('trim') | reject('equalto', '') | list | count }} - {% endif %} + {% set down_items = state_attr('sensor.docker_containers_down_list', 'down_containers') | default([], true) | list %} + {{ down_items | count }} - name: "Docker Stacks Down List" unique_id: docker_stacks_down_list @@ -475,6 +514,60 @@ script: data: topic: "DOCKER" message: "{{ container_name }} is {{ effective_state }} for over 5 minutes." + - delay: + minutes: 15 + - variables: + persistent_effective_state: >- + {% set candidates = [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %} + {% set resolver = namespace(state='unknown', chosen=false) %} + {% for candidate in candidates %} + {% if not resolver.chosen and expand(candidate) | count > 0 %} + {% set candidate_state = states(candidate) | lower %} + {% if candidate_state not in ['unknown', 'unavailable', ''] %} + {% set resolver.state = candidate_state %} + {% set resolver.chosen = true %} + {% endif %} + {% endif %} + {% endfor %} + {% if not resolver.chosen %} + {% for candidate in candidates %} + {% if not resolver.chosen and expand(candidate) | count > 0 %} + {% set resolver.state = states(candidate) | lower %} + {% set resolver.chosen = true %} + {% endif %} + {% endfor %} + {% endif %} + {{ resolver.state }} + container_name: "{{ state_attr(effective_entity, 'friendly_name') | default(container_key, true) }}" + - condition: template + value_template: >- + {{ persistent_effective_state in down_states and + not (is_state('binary_sensor.docker_container_telemetry_degraded', 'on') and + persistent_effective_state in ['unknown', 'unavailable']) }} + - condition: state + entity_id: binary_sensor.docker_container_alerts_snoozed + state: "off" + - service: script.joanna_dispatch + data: + trigger_context: >- + HA automation docker_state_sync_repairs_dynamic + (Docker State Sync - Repairs (Dynamic)) + source: "home_assistant_automation.docker_state_sync_repairs_dynamic" + summary: "{{ container_name }} container has remained {{ persistent_effective_state }} for 20 minutes" + entity_ids: + - "{{ effective_entity }}" + - "{{ switch_entity }}" + diagnostics: >- + issue_id={{ issue_id }}, + spook_issue_id={{ spook_issue_id }}, + container_key={{ container_key }}, + effective_entity={{ effective_entity }}, + switch_entity={{ switch_entity }}, + effective_state_initial={{ effective_state }}, + effective_state_20m={{ persistent_effective_state }} + request: >- + Troubleshoot and resolve the persistent Docker container outage if possible. + Use Duplicati and the related host/container telemetry to verify recovery. - conditions: "{{ op == 'clear' }}" sequence: - variables: @@ -695,37 +788,54 @@ automation: - alias: "Docker State Sync - Repairs (Dynamic)" id: docker_state_sync_repairs_dynamic description: "Detect Docker container/stack state transitions and delegate Repairs sync." - mode: parallel + mode: queued + max: 50 + max_exceeded: silent trigger: - platform: event event_type: state_changed + variables: + entity_id: "{{ trigger.event.data.entity_id | default('') }}" + old_state: "{{ (trigger.event.data.old_state.state if trigger.event.data.old_state is not none else '') | lower }}" + new_state: "{{ (trigger.event.data.new_state.state if trigger.event.data.new_state is not none else '') | lower }}" + monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list }}" + is_monitored_container_event: >- + {% set ent = entity_id %} + {% if ent.startswith('switch.') and (ent.endswith('_container') or ent.endswith('_container_2')) %} + {{ ent in monitored_switches }} + {% elif ent.startswith('binary_sensor.') and (ent.endswith('_status') or ent.endswith('_status_2')) %} + {% set key = ent | replace('binary_sensor.', '') | regex_replace('_status(?:_2)?$', '') %} + {{ ('switch.' ~ key ~ '_container') in monitored_switches or ('switch.' ~ key ~ '_container_2') in monitored_switches }} + {% elif ent.startswith('sensor.') and (ent.endswith('_state') or ent.endswith('_state_2')) %} + {% set key = ent | replace('sensor.', '') | regex_replace('_state(?:_2)?$', '') %} + {{ ('switch.' ~ key ~ '_container') in monitored_switches or ('switch.' ~ key ~ '_container_2') in monitored_switches }} + {% else %} + false + {% endif %} + is_monitored_stack_event: >- + {% set ent = entity_id %} + {% if ent.startswith('binary_sensor.') and ent.endswith('_stack_status') %} + {% set stack_key = ent | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %} + {{ expand('sensor.' ~ stack_key ~ '_stack_containers_count') | count > 0 }} + {% else %} + false + {% endif %} condition: - condition: template value_template: "{{ trigger.event.data.old_state is not none and trigger.event.data.new_state is not none }}" - condition: template - value_template: "{{ trigger.event.data.old_state.state != trigger.event.data.new_state.state }}" + value_template: "{{ old_state != new_state }}" + - condition: template + value_template: "{{ is_monitored_container_event or is_monitored_stack_event }}" action: - - variables: - entity_id: "{{ trigger.event.data.entity_id | default('') }}" - old_state: "{{ trigger.event.data.old_state.state | lower }}" - new_state: "{{ trigger.event.data.new_state.state | lower }}" - monitored: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) }}" + - delay: + seconds: 2 + - condition: template + value_template: "{{ states(entity_id) | lower == new_state }}" - choose: - conditions: - condition: template - value_template: >- - {% set ent = entity_id %} - {% if ent.startswith('switch.') and (ent.endswith('_container') or ent.endswith('_container_2')) %} - {{ ent in monitored }} - {% elif ent.startswith('binary_sensor.') and (ent.endswith('_status') or ent.endswith('_status_2')) %} - {% set key = ent | replace('binary_sensor.', '') | regex_replace('_status(?:_2)?$', '') %} - {{ ('switch.' ~ key ~ '_container') in monitored or ('switch.' ~ key ~ '_container_2') in monitored }} - {% elif ent.startswith('sensor.') and (ent.endswith('_state') or ent.endswith('_state_2')) %} - {% set key = ent | replace('sensor.', '') | regex_replace('_state(?:_2)?$', '') %} - {{ ('switch.' ~ key ~ '_container') in monitored or ('switch.' ~ key ~ '_container_2') in monitored }} - {% else %} - false - {% endif %} + value_template: "{{ is_monitored_container_event }}" sequence: - variables: down_states: ['off', 'stopped', 'exited', 'dead', 'unknown', 'unavailable'] @@ -748,14 +858,7 @@ automation: operation: "clear" - conditions: - condition: template - value_template: >- - {% set ent = entity_id %} - {% if ent.startswith('binary_sensor.') and ent.endswith('_stack_status') %} - {% set stack_key = ent | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %} - {{ expand('sensor.' ~ stack_key ~ '_stack_containers_count') | count > 0 }} - {% else %} - false - {% endif %} + value_template: "{{ is_monitored_stack_event }}" sequence: - variables: down_states: ['off', 'unknown', 'unavailable'] @@ -785,7 +888,7 @@ automation: minutes: "/55" action: - variables: - monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) }}" + monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list }}" - repeat: for_each: "{{ monitored_switches }}" sequence: @@ -805,7 +908,7 @@ automation: {% endif %} {% endif %} {% endfor %} - {{ ns.items }} + {{ ns.items | list }} - repeat: for_each: "{{ stack_status_entities }}" sequence: @@ -832,7 +935,9 @@ automation: data: title: "Docker Maintenance Check" value1: "{{ states('sensor.docker_containers_down_count') }} containers are currently down." - value2: "Down: {{ states('sensor.docker_containers_down_list') }}" + value2: >- + {% set down_items = state_attr('sensor.docker_containers_down_list', 'down_containers') | default([], true) | list %} + Down: {{ down_items | join(', ') if (down_items | count > 0) else 'none' }} who: "carlo" group: "maintenance" title1: "Yes, snooze 1h" @@ -846,7 +951,8 @@ automation: topic: "DOCKER" message: >- Maintenance prompt sent to Carlo ({{ states('sensor.docker_containers_down_count') }} down: - {{ states('sensor.docker_containers_down_list') }}). + {% set down_items = state_attr('sensor.docker_containers_down_list', 'down_containers') | default([], true) | list %} + {{ down_items | join(', ') if (down_items | count > 0) else 'none' }}). - alias: "Docker Maintenance Snooze 1H" id: docker_maintenance_snooze_1h @@ -885,7 +991,9 @@ automation: topic: "DOCKER" message: >- Maintenance snooze declined with {{ states('sensor.docker_containers_down_count') }} - containers down ({{ states('sensor.docker_containers_down_list') }}). + containers down ( + {% set down_items = state_attr('sensor.docker_containers_down_list', 'down_containers') | default([], true) | list %} + {{ down_items | join(', ') if (down_items | count > 0) else 'none' }}). - alias: "Docker Telemetry Template Refresh" id: docker_telemetry_template_refresh diff --git a/config/packages/onenote_indexer.yaml b/config/packages/onenote_indexer.yaml index 2b876286..b03b80ef 100644 --- a/config/packages/onenote_indexer.yaml +++ b/config/packages/onenote_indexer.yaml @@ -11,7 +11,7 @@ # Notes: Only explicit last_status='error' is treated as failure; unknown/unavailable are neutral. # Notes: HA->Joanna request includes trigger context so Telegram progress messages can identify origin. # Notes: Creates/clears a Spook Repair issue and requests Joanna remediation on failures. -# Notes: Schedules daily OneNote duplicate-delete maintenance request via Joanna. +# Notes: Daily Joanna recap should be plain-English; only surface detailed index metrics when something materially changes or fails. ###################################################################### sensor: @@ -138,7 +138,9 @@ automation: diagnostics: "scheduled_time=03:15:00" request: >- Run OneNote duplicate cleanup and apply deletions. - Include delete reconciliation and report pages/chunks and delete metrics before and after completion. + Include delete reconciliation, then send a brief conversational morning summary. + Tell me whether cleanup finished and whether anything actually changed. + Only include detailed counts or backend metrics if pages were removed, something failed, or you need my attention. - id: onenote_indexer_failure_open_repair alias: OneNote Indexer - Open Repair On Failure diff --git a/config/packages/space.yaml b/config/packages/space.yaml index 98da41ca..c5aa455c 100755 --- a/config/packages/space.yaml +++ b/config/packages/space.yaml @@ -1,10 +1,12 @@ -#------------------------------------------- -# @CCOSTAN -# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig -# Space - ISS and space-related sensors. -#------------------------------------------- ###################################################################### -## Track space events and ISS passes. +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Space - ISS and launch-related telemetry +# Tracks SpaceX timing and announces an upcoming launch window. +# ------------------------------------------------------------------- +# Notes: Guard template triggers against unavailable startup sensor state. ###################################################################### sensor: @@ -32,11 +34,16 @@ automation: id: 1d42fc4f-a37d-4283-b64b-09242a145598 trigger: - platform: template - value_template: '{{ (now().strftime("%s") | int + 600) == (states.sensor.spacex.state | int) }}' + value_template: >- + {% set launch_ts = states('sensor.spacex') | int(0) %} + {% set seconds_until = launch_ts - (now().timestamp() | int) %} + {{ launch_ts > 0 and seconds_until > 0 and seconds_until <= 600 }} action: - service: script.notify_engine data: - value1: 'Go Outside! There is a Rocket Launch very soon! {{states.sensor.next_launch.attributes.stream }}' + value1: >- + {% set mission = state_attr('sensor.spacex', 'mission_name') | default('A SpaceX launch', true) %} + Go outside. {{ mission }} is scheduled to launch in about 10 minutes. group: 'information' diff --git a/config/packages/stats.yaml b/config/packages/stats.yaml index f4b3c6bf..d65bc20d 100755 --- a/config/packages/stats.yaml +++ b/config/packages/stats.yaml @@ -14,7 +14,8 @@ command_line: - sensor: name: 'Lines of Code' unique_id: lines_of_code - command: "find /config -name '*.yaml' | xargs cat | wc -l" + command: "find /config -type f -name '*.yaml' -exec wc -l {} + | awk 'END {print $1}'" + command_timeout: 60 scan_interval: 20000 value_template: "{{ value | int }}" unit_of_measurement: "count" diff --git a/config/script/README.md b/config/script/README.md index 16d47d46..876b1e8f 100755 --- a/config/script/README.md +++ b/config/script/README.md @@ -48,6 +48,7 @@ Current automations that kick off automated resolutions (via `script.joanna_disp | `mqtt_open_repair_on_failure` | MQTT - Open Repair On Failure | [../packages/mqtt_status.yaml](../packages/mqtt_status.yaml) | | `onenote_indexer_daily_delete_maintenance` | OneNote Indexer - Daily Delete Maintenance Request | [../packages/onenote_indexer.yaml](../packages/onenote_indexer.yaml) | | `onenote_indexer_failure_open_repair` | OneNote Indexer - Open Repair On Failure | [../packages/onenote_indexer.yaml](../packages/onenote_indexer.yaml) | +| `docker_state_sync_repairs_dynamic` | Docker State Sync - Repairs (Dynamic) | [../packages/docker_infrastructure.yaml](../packages/docker_infrastructure.yaml) | | `unifi_ap_no_clients_repair_combined` | Unifi AP Create Repair Issue after 5m of 0 Clients | [../packages/wireless.yaml](../packages/wireless.yaml) | ### Tips