From 539ede858666809a34543c89b8621966d69b644b Mon Sep 17 00:00:00 2001 From: Carlo Costanzo Date: Wed, 13 May 2026 11:13:49 -0400 Subject: [PATCH] Update Joanna dispatch and infra health checks --- config/.HA_VERSION | 2 +- .../.cache/brands/integrations/unifi/icon.png | Bin 14253 -> 15927 bytes config/packages/README.md | 6 +- config/packages/bearclaw.yaml | 5 + config/packages/docker_infrastructure.yaml | 6 + config/packages/infrastructure.yaml | 141 +++++++++++------- config/packages/onenote_indexer.yaml | 79 +++++++++- config/packages/synology_dsm.yaml | 98 +++++++++--- config/script/README.md | 3 +- config/script/joanna_dispatch.yaml | 9 ++ config/templates/speech/briefing.yaml | 25 ++-- 11 files changed, 275 insertions(+), 99 deletions(-) diff --git a/config/.HA_VERSION b/config/.HA_VERSION index 5c20f85f..92a9c854 100755 --- a/config/.HA_VERSION +++ b/config/.HA_VERSION @@ -1 +1 @@ -2026.5.0 \ No newline at end of file +2026.5.1 \ No newline at end of file diff --git a/config/.cache/brands/integrations/unifi/icon.png b/config/.cache/brands/integrations/unifi/icon.png index 4e1c9c6f4d3363a84c92a58becc9a9a6161467cc..bdc8d81e455c52279aa6aa478cf2f7d2cc660a42 100644 GIT binary patch literal 15927 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 AX8-^I diff --git a/config/packages/README.md b/config/packages/README.md index 9793672d..48d780ed 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -48,9 +48,9 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [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, 20-minute Joanna escalation for persistent container outages using stable configured monitor membership, 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 + Joanna dispatch for sustained node degradations, plus nightly Frigate reboot. | `binary_sensor.proxmox*_runtime_healthy`, `sensor.proxmox*_disk_used_percentage`, `repairs.create`, `script.joanna_dispatch`, `button.qemu_docker2_101_reboot` | -| [synology_dsm.yaml](synology_dsm.yaml) | Synology DSM integration health normalization for Carlo-NAS01 and Carlo-NVR, with Repairs + Joanna dispatch on sustained integration, security, or storage problems. | `binary_sensor.carlo_*_synology_problem`, `sensor.carlo_*_synology_problem_summary`, `repairs.create`, `script.joanna_dispatch` | -| [infrastructure.yaml](infrastructure.yaml) | Normalized WAN/DNS/backup/domain/cert health, Glances-backed Docker host disk pressure with bounded safe Joanna cleanup, and website uptime/latency SLO signals for Infrastructure dashboards, plus nightly backup verification and monthly Joanna HA log hygiene review with GitHub issue follow-up. | `sensor.docker_*_disk_used_percentage`, `automation.docker_host_disk_pressure_monitor`, `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `automation.infra_backup_nightly_verification`, `script.joanna_dispatch` | -| [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` | +| [synology_dsm.yaml](synology_dsm.yaml) | Synology DSM integration health normalization for Carlo-NAS01 and Carlo-NVR, with outage-aware Joanna-first handling for lone post-outage volume warnings and Repairs escalation for persistent or non-outage problems. | `binary_sensor.carlo_*_synology_problem`, `sensor.carlo_*_synology_problem_summary`, `binary_sensor.powerwall_grid_status`, `repairs.create`, `script.joanna_dispatch` | +| [infrastructure.yaml](infrastructure.yaml) | Normalized WAN/DNS/backup/domain/cert health, Glances-backed Docker host disk pressure with Joanna-only warning cleanup and critical Repairs, and website uptime/latency SLO signals for Infrastructure dashboards, plus nightly backup verification and monthly Joanna HA log hygiene review with GitHub issue follow-up. | `sensor.docker_*_disk_used_percentage`, `automation.docker_host_disk_pressure_monitor`, `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `automation.infra_backup_nightly_verification`, `script.joanna_dispatch` | +| [onenote_indexer.yaml](onenote_indexer.yaml) | OneNote indexer health/status monitoring for Joanna, explicit index-health confirmation, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful`, `binary_sensor.onenote_indexer_index_healthy` | | [mqtt_status.yaml](mqtt_status.yaml) | Command-line MQTT broker reachability probe with Spook Repairs escalation and Joanna troubleshooting dispatch on outage. | `binary_sensor.mqtt_status_raw`, `binary_sensor.mqtt_broker_problem`, `repairs.create`, `rest_command.bearclaw_command` | | [mariadb.yaml](mariadb.yaml) | MariaDB recorder health and capacity snapshots with hourly live metrics, weekly admin/recorder polling, and stats-ready numeric sensors. | `sensor.mariadb_status`, `sensor.database_size` | | [processmonitor.yaml](processmonitor.yaml) | Root filesystem disk-pressure monitoring with immediate digest/logbook notes at 80%, Joanna review after 10 minutes above 80%, and delayed phone alerts only if the issue stays unresolved after dispatch. | `sensor.disk_use_percent`, `repairs.create`, `script.joanna_dispatch`, `tts.clear_cache` | diff --git a/config/packages/bearclaw.yaml b/config/packages/bearclaw.yaml index 2923438c..237ff4c8 100644 --- a/config/packages/bearclaw.yaml +++ b/config/packages/bearclaw.yaml @@ -16,6 +16,7 @@ # Notes: v2 intake is the primary HA contract; legacy command/ingest routes remain appliance-side shims. # Notes: Command payload supports async_only for automation-first queueing when immediate inline handling is not required. # Notes: Command payload supports optional metadata for HA dispatch context snapshots. +# Notes: HA automation dispatches default to BearClaw's ops domain so wording like NAS "health" cannot route to the health coach. # Notes: Blog: https://www.vcloudinfo.com/2026/03/joanna-dispatch-telemetry-home-assistant-infrastructure-dashboard/ ###################################################################### @@ -44,6 +45,10 @@ rest_command: "context": {{ context | default(none) | tojson }}, "callback": {{ callback | default(none) | tojson }} }, + "routing": { + "domainHint": {{ domain_hint | default('ops', true) | tojson }}, + "laneHint": {{ lane_hint | default('joanna.ops', true) | tojson }} + }, "replyTargets": [ { "type": "ha", diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index e68c2f3c..25269f4f 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -16,6 +16,7 @@ # Notes: Outage escalation keys off the configured monitored group so host-wide telemetry drops do not fall out of scope before the delayed Joanna dispatch runs. # Notes: Weekly reconcile should replace retired container-name switches with the current container-ID-prefixed discovery set. # Notes: Tapple is now served by `games_hub` on `/tapple/`; do not keep a standalone `tapple` container switch in the monitored group. +# Notes: Teslamate and crystalsoftwashsolutions are live services and should remain in the monitored group when their discovery switches are present. # Notes: Infra Info was removed; BearClaw Admin is the planning snapshot surface. ###################################################################### @@ -86,6 +87,7 @@ switch: - switch.college_budget_app_container_2 - switch.cruise_tracker_container - switch.cruise_tracker_container_2 + - switch.crystalsoftwashsolutions_container - switch.dashy_container - switch.dashy_container_2 - switch.docker_socket_proxy_container @@ -149,6 +151,10 @@ switch: - switch.redis_webhooks_engine_container_2 - switch.rvtools_ppt_web_container - switch.rvtools_ppt_web_container_2 + - switch.teslamate_backup_container + - switch.teslamate_container + - switch.teslamate_database_container + - switch.teslamate_grafana_container - switch.tugtainer_agent_container - switch.tugtainer_agent_container_2 - switch.tugtainer_container diff --git a/config/packages/infrastructure.yaml b/config/packages/infrastructure.yaml index 1a92e277..03ad70a2 100644 --- a/config/packages/infrastructure.yaml +++ b/config/packages/infrastructure.yaml @@ -11,10 +11,12 @@ # Notes: Domain warning threshold is <30 days; critical threshold is <14 days. # Notes: Nightly Duplicati verification runs at 08:00 after the 05:30 Duplicati job and docker_14 reboot window. # Notes: Duplicati transport/API errors are logged only; repairs are reserved for proven failed or stale backups. +# Notes: Duplicati failure Repairs enable a recovery poll that clears the Repair after a later successful run. # Notes: Monthly HA log hygiene review requests Telegram + GitHub issue follow-up only; Joanna must wait for approval before any changes. # Notes: Numeric WAN telemetry exposes state_class so recorder can keep long-term statistics. # Notes: Docker host root disk usage uses Glances-backed normalized sensors; raw Glances sensors are recorder/logbook-filtered. # Notes: Disk-pressure dispatch allows bounded safe cleanup of disposable caches and old generated backup artifacts, but not live data or restarts. +# Notes: Warning-level Docker host disk pressure is Joanna-only; Repairs are reserved for critical pressure. ###################################################################### input_text: @@ -28,6 +30,10 @@ input_text: name: "docker_69 disk pressure band" max: 20 +input_boolean: + infra_duplicati_backup_repair_active: + name: "Duplicati backup repair active" + command_line: - sensor: name: Infra WAN Packet Loss @@ -481,15 +487,10 @@ automation: value: "critical" - conditions: "{{ current_band == 'warning' and previous_band not in ['warning', 'critical'] }}" sequence: - - service: repairs.create + - service: repairs.remove + continue_on_error: true data: issue_id: "{{ issue_id }}" - severity: warning - persistent: true - title: "{{ host_name }} disk pressure warning ({{ disk_pct | round(1) }}%)" - description: >- - {{ host_name }} root disk usage is elevated. - Plan cleanup before capacity reaches critical levels. - service: script.joanna_dispatch data: trigger_context: "HA automation docker_host_disk_pressure_monitor (Docker Host Disk Pressure Monitor - Warning)" @@ -520,7 +521,7 @@ automation: topic: "DOCKER" message: >- {{ host_name }} disk usage warning at {{ disk_pct | round(1) }}%. - Repair {{ issue_id }} opened and Joanna investigation requested. + Joanna investigation requested without opening a warning Repair. - service: input_text.set_value target: entity_id: "{{ band_entity }}" @@ -528,19 +529,14 @@ automation: value: "warning" - conditions: "{{ current_band == 'warning' and previous_band == 'critical' }}" sequence: - - service: repairs.create + - service: repairs.remove + continue_on_error: true data: issue_id: "{{ issue_id }}" - severity: warning - persistent: true - title: "{{ host_name }} disk pressure warning ({{ disk_pct | round(1) }}%)" - description: >- - {{ host_name }} root disk usage is elevated but no longer critical. - Continue cleanup before capacity reaches critical levels again. - service: script.send_to_logbook data: topic: "DOCKER" - message: "{{ host_name }} disk usage dropped from critical to warning at {{ disk_pct | round(1) }}%." + message: "{{ host_name }} disk usage dropped from critical to warning at {{ disk_pct | round(1) }}%. Critical Repair cleared; Joanna continues handling warning-level cleanup." - service: input_text.set_value target: entity_id: "{{ band_entity }}" @@ -580,13 +576,27 @@ automation: trigger: - platform: time at: "08:00:00" + id: nightly + - platform: time_pattern + minutes: "15" + id: recovery_poll + - platform: time_pattern + minutes: "45" + id: recovery_poll + condition: + - condition: template + value_template: >- + {{ trigger is not defined or trigger.id != 'recovery_poll' + or is_state('input_boolean.infra_duplicati_backup_repair_active', 'on') }} action: - variables: + trigger_source: "{{ trigger.id if trigger is defined and trigger.id is defined else 'manual' }}" + verifier_reason: "{{ 'ha_failure_followup' if trigger_source == 'recovery_poll' else 'ha_nightly' }}" trigger_context: "HA automation infra_backup_nightly_verification (Infrastructure - Backup Nightly Verification)" duplicati_state: "{{ states('switch.duplicati_container') }}" - action: rest_command.bearclaw_duplicati_verify data: - reason: "ha_nightly" + reason: "{{ verifier_reason }}" response_variable: duplicati_verify - service: script.send_to_logbook data: @@ -618,6 +628,9 @@ automation: continue_on_error: true data: issue_id: user_infra_duplicati_backup_failure + - service: input_boolean.turn_off + target: + entity_id: input_boolean.infra_duplicati_backup_repair_active - conditions: "{{ verify_transport_issue }}" sequence: - service: script.send_to_logbook @@ -628,46 +641,60 @@ automation: status {{ verify_status }} with issue {{ verify_issue }}. No repair card was opened because this is verifier transport state, not a confirmed backup failure. default: - - service: repairs.create - data: - issue_id: infra_duplicati_backup_failure - title: "Duplicati nightly backup verification failed" - description: >- - {{ verify_summary }} - Backup={{ verify_backup_name }}; - status={{ verify_status }}; - last_result={{ verify_latest_result.get('endedAt', 'n/a') }}; - last_success={{ verify_last_success.get('endedAt', 'n/a') }}. - severity: error - persistent: true - - service: script.joanna_dispatch - data: - trigger_context: "{{ trigger_context }}" - source: "home_assistant_automation.infra_backup_nightly_verification" - summary: "Nightly Duplicati backup verification failed" - entity_ids: - - "switch.duplicati_container" - diagnostics: >- - scheduled_time=08:00:00, - duplicati_container={{ duplicati_state }}, - verifier_http_status={{ verify_http_status }}, - verifier_status={{ verify_status }}, - verifier_issue={{ verify_issue }}, - backup_name={{ verify_backup_name }}, - latest_result={{ verify_latest_result.get('endedAt', 'n/a') }}, - last_success={{ verify_last_success.get('endedAt', 'n/a') }} - request: >- - Investigate the Duplicati backup job {{ verify_backup_name }}. - The codex_appliance verifier reported status {{ verify_status }} with issue {{ verify_issue }}. - Use the Duplicati API or UI directly, resolve the failure if possible, and verify a successful run before closing out. - Reply with explicit status fields: - resolved=true/false, - backup_status, - last_success_time, - root_cause, - action_taken, - verification, - next_action_required=true/false. + - service: input_boolean.turn_on + target: + entity_id: input_boolean.infra_duplicati_backup_repair_active + - choose: + - conditions: "{{ trigger_source != 'recovery_poll' }}" + sequence: + - service: repairs.create + data: + issue_id: infra_duplicati_backup_failure + title: "Duplicati nightly backup verification failed" + description: >- + {{ verify_summary }} + Backup={{ verify_backup_name }}; + status={{ verify_status }}; + last_result={{ verify_latest_result.get('endedAt', 'n/a') }}; + last_success={{ verify_last_success.get('endedAt', 'n/a') }}. + severity: error + persistent: true + - service: script.joanna_dispatch + data: + trigger_context: "{{ trigger_context }}" + source: "home_assistant_automation.infra_backup_nightly_verification" + summary: "Nightly Duplicati backup verification failed" + entity_ids: + - "switch.duplicati_container" + diagnostics: >- + scheduled_time=08:00:00, + duplicati_container={{ duplicati_state }}, + verifier_http_status={{ verify_http_status }}, + verifier_status={{ verify_status }}, + verifier_issue={{ verify_issue }}, + backup_name={{ verify_backup_name }}, + latest_result={{ verify_latest_result.get('endedAt', 'n/a') }}, + last_success={{ verify_last_success.get('endedAt', 'n/a') }} + request: >- + Investigate the Duplicati backup job {{ verify_backup_name }}. + The codex_appliance verifier reported status {{ verify_status }} with issue {{ verify_issue }}. + Use the Duplicati API or UI directly, resolve the failure if possible, and verify a successful run before closing out. + Home Assistant will re-check this verifier every 30 minutes after dispatch and clear the Repair automatically once the backup is healthy. + Reply with explicit status fields: + resolved=true/false, + backup_status, + last_success_time, + root_cause, + action_taken, + verification, + next_action_required=true/false. + default: + - service: script.send_to_logbook + data: + topic: "BACKUP" + message: >- + Duplicati recovery follow-up still reports {{ verify_status }} for {{ verify_backup_name }}: + {{ verify_issue }}. Existing Repair remains open; Joanna was not dispatched again. - alias: "Infrastructure - Monthly HA Log Hygiene Review" id: infra_monthly_log_hygiene_review diff --git a/config/packages/onenote_indexer.yaml b/config/packages/onenote_indexer.yaml index febe3775..2e05f449 100644 --- a/config/packages/onenote_indexer.yaml +++ b/config/packages/onenote_indexer.yaml @@ -7,10 +7,12 @@ # Polls codex_appliance OneNote status and exposes trigger-ready health entities. # ------------------------------------------------------------------- # Notes: Keep onenote indexer monitoring in this package (separate from bearclaw transport). -# Notes: last_status='never' is treated as success to avoid false alerts after restarts. +# Notes: last_status='never' is treated as success only when index health is confirmed. # 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: Index health requires pages, chunks, no pending embeddings, and a healthy embedding worker. +# Notes: Recovery clear is polled so stale Repairs do not linger after the indexer recovers. # Notes: Daily Joanna recap should be plain-English; only surface detailed index metrics when something materially changes or fails. ###################################################################### @@ -43,13 +45,23 @@ template: state: >- {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set sync = payload.get('sync', {}) if payload is mapping else {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} {% set raw = (sync.get('last_status', '') | string | lower) %} - {% if raw in ['ok', 'success', 'never'] %} + {% set pages = index.get('pages') | int(0) %} + {% set chunks = index.get('chunks') | int(0) %} + {% set pending = index.get('pending_embeddings') | int(999999) %} + {% set worker_status = worker.get('lastStatus', '') | string | lower %} + {% set worker_running = worker.get('running', false) | bool %} + {% set index_healthy = pages > 0 and chunks > 0 and pending == 0 and worker_status == 'ok' and not worker_running %} + {% if raw in ['ok', 'success'] or (raw == 'never' and index_healthy) %} success {% elif raw == 'running' %} running {% elif raw == 'error' %} error + {% elif raw == 'never' %} + unknown {% else %} unknown {% endif %} @@ -86,6 +98,12 @@ template: {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set index = payload.get('index', {}) if payload is mapping else {} %} {{ index.get('chunks') }} + embedding_worker_status: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastStatus') }} + embedding_worker_last_run_at: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastRunAt') }} last_metrics: >- {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set sync = payload.get('sync', {}) if payload is mapping else {} %} @@ -103,6 +121,44 @@ template: mdi:alert-circle {% endif %} + - name: OneNote Indexer Index Healthy + unique_id: onenote_indexer_index_healthy + state: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {% set pages = index.get('pages') | int(0) %} + {% set chunks = index.get('chunks') | int(0) %} + {% set pending = index.get('pending_embeddings') | int(999999) %} + {% set worker_status = worker.get('lastStatus', '') | string | lower %} + {% set worker_running = worker.get('running', false) | bool %} + {{ pages > 0 and chunks > 0 and pending == 0 and worker_status == 'ok' and not worker_running }} + icon: >- + {% if is_state('binary_sensor.onenote_indexer_index_healthy', 'on') %} + mdi:notebook-check + {% else %} + mdi:notebook-remove + {% endif %} + attributes: + pages: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('pages') }} + chunks: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('chunks') }} + pending_embeddings: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('pending_embeddings') }} + embedding_worker_status: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastStatus') }} + embedding_worker_last_run_at: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastRunAt') }} + - name: OneNote Indexer Job Failed unique_id: onenote_indexer_job_failed device_class: problem @@ -199,13 +255,28 @@ automation: - id: onenote_indexer_failure_clear_repair alias: OneNote Indexer - Clear Repair On Recovery - description: Clear the Spook Repair issue when OneNote indexer is healthy again. + description: Clear the Spook Repair issue when OneNote indexer and index health are confirmed healthy again. mode: single trigger: - platform: state entity_id: binary_sensor.onenote_indexer_job_failed to: "off" for: "00:02:00" + - platform: state + entity_id: binary_sensor.onenote_indexer_index_healthy + to: "on" + for: "00:02:00" + - platform: time_pattern + minutes: "20" + - platform: time_pattern + minutes: "50" + condition: + - condition: state + entity_id: binary_sensor.onenote_indexer_job_failed + state: "off" + - condition: state + entity_id: binary_sensor.onenote_indexer_index_healthy + state: "on" action: - service: repairs.remove continue_on_error: true @@ -214,4 +285,4 @@ automation: - service: script.send_to_logbook data: topic: "ONENOTE" - message: "OneNote indexer recovered. Spook repair cleared." + message: "OneNote indexer and index health are confirmed healthy. Spook repair cleared." diff --git a/config/packages/synology_dsm.yaml b/config/packages/synology_dsm.yaml index ba4e5598..9704836d 100644 --- a/config/packages/synology_dsm.yaml +++ b/config/packages/synology_dsm.yaml @@ -9,6 +9,7 @@ # Notes: Uses native `synology_dsm` entities for Carlo-NAS01 and Carlo-NVR. # Notes: Joanna dispatches are reserved for integration/security/storage problems, not routine reboot/shutdown controls. # Notes: DSM update availability stays diagnostic context only; it does not trigger remediation by itself. +# Notes: Recent Powerwall outages route lone volume warnings to Joanna first; Repairs open after the recovery grace window if still active. ###################################################################### template: @@ -281,16 +282,47 @@ template: automation: - id: synology_dsm_open_repair_and_dispatch alias: "Synology DSM - Open Repair And Dispatch" - description: "Open a Repairs issue and dispatch Joanna when a Synology problem stays active." + description: "Dispatch Joanna when a Synology problem stays active, and open Repairs after outage-aware grace checks." mode: queued trigger: - - platform: state + - id: initial_dispatch + platform: state entity_id: - binary_sensor.carlo_nas01_synology_problem - binary_sensor.carlo_nvr_synology_problem to: "on" for: "00:10:00" + - id: repair_escalation + platform: state + entity_id: + - binary_sensor.carlo_nas01_synology_problem + - binary_sensor.carlo_nvr_synology_problem + to: "on" + for: "01:00:00" variables: + outage_grace_minutes: 60 + trigger_phase: "{{ trigger.id | default('initial_dispatch', true) }}" + is_repair_escalation: "{{ trigger_phase == 'repair_escalation' }}" + grid_state: "{{ states('binary_sensor.powerwall_grid_status') }}" + grid_changed_minutes: >- + {% if states.binary_sensor.powerwall_grid_status is defined %} + {{ ((as_timestamp(now(), 0) - as_timestamp(states.binary_sensor.powerwall_grid_status.last_changed, 0)) / 60) | round(1) }} + {% else %} + 9999 + {% endif %} + outage_grace_active: >- + {{ grid_state == 'off' or + (grid_state == 'on' and (grid_changed_minutes | float(9999)) <= outage_grace_minutes) }} + outage_context: >- + {% if states.binary_sensor.powerwall_grid_status is not defined %} + Powerwall grid status entity is unavailable to this automation. + {% elif grid_state == 'off' %} + Powerwall grid is currently down; outage began {{ states.binary_sensor.powerwall_grid_status.last_changed }}. + {% elif (grid_changed_minutes | float(9999)) <= outage_grace_minutes %} + Powerwall grid recovered {{ grid_changed_minutes }} minutes ago. + {% else %} + No recent Powerwall outage recovery within {{ outage_grace_minutes }} minutes. + {% endif %} host_name: >- {% if trigger.entity_id == 'binary_sensor.carlo_nas01_synology_problem' %} Carlo-NAS01 @@ -402,32 +434,49 @@ automation: volume_status: "{{ states(volume_status_entity) }}" volume_used: "{{ states(volume_used_entity) }}" dsm_update_state: "{{ states(update_entity) }}" + lone_volume_warning: >- + {{ problem_summary | lower | trim == 'volume status=warning' and + volume_status | lower | trim == 'warning' and + security_state == 'off' }} + joanna_only_outage_grace: >- + {{ not (is_repair_escalation | bool(false)) and + (outage_grace_active | bool(false)) and + (lone_volume_warning | bool(false)) }} + should_create_repair: "{{ not (joanna_only_outage_grace | bool(false)) }}" trigger_context: "HA automation synology_dsm_open_repair_and_dispatch (Synology DSM - Open Repair And Dispatch)" action: - - service: repairs.create - data: - issue_id: "{{ issue_id }}" - title: "{{ host_name }} Synology health issue" - severity: "{{ 'error' if problem_severity == 'error' else 'warning' }}" - persistent: true - description: >- - Home Assistant detected a sustained Synology DSM issue for {{ host_name }}. + - choose: + - conditions: + - condition: template + value_template: "{{ should_create_repair | bool(false) }}" + sequence: + - service: repairs.create + data: + issue_id: "{{ issue_id }}" + title: "{{ host_name }} Synology health issue" + severity: "{{ 'error' if problem_severity == 'error' else 'warning' }}" + persistent: true + description: >- + Home Assistant detected a sustained Synology DSM issue for {{ host_name }}. - summary: {{ problem_summary }} - security_state: {{ security_state }} - volume_status: {{ volume_status }} - volume_used: {{ volume_used }} - dsm_update: {{ dsm_update_state }} - ssh_alias: {{ ssh_alias }} - dsm_url: {{ dsm_url }} + summary: {{ problem_summary }} + security_state: {{ security_state }} + volume_status: {{ volume_status }} + volume_used: {{ volume_used }} + dsm_update: {{ dsm_update_state }} + outage_context: {{ outage_context }} + ssh_alias: {{ ssh_alias }} + dsm_url: {{ dsm_url }} - service: script.joanna_dispatch data: trigger_context: "{{ trigger_context }}" source: "{{ source }}" - summary: "{{ host_name }} Synology DSM problem detected" + summary: >- + {{ host_name }} Synology DSM problem detected{{ ' after recent Powerwall outage' if joanna_only_outage_grace | bool(false) else '' }} entity_ids: "{{ entity_ids }}" diagnostics: >- issue_id={{ issue_id }}, + trigger_phase={{ trigger_phase }}, severity={{ problem_severity }}, problem_sensor={{ trigger.entity_id }}, problem_summary={{ problem_summary }}, @@ -435,18 +484,27 @@ automation: volume_status={{ volume_status }}, volume_used={{ volume_used }}, dsm_update={{ dsm_update_state }}, + outage_grace_active={{ outage_grace_active }}, + outage_context={{ outage_context }}, + joanna_only_outage_grace={{ joanna_only_outage_grace }}, + repair_created={{ should_create_repair }}, ssh_alias={{ ssh_alias }}, dsm_url={{ dsm_url }} request: >- Investigate {{ host_name }} using the Home Assistant Synology DSM entities first, then DSM or SSH if needed. - Review security status, drive health, volume health, and integration availability. + Review security state, drive condition, volume condition, and integration availability. + If this is a recent Powerwall outage and the only symptom is a volume warning, treat it as post-outage recovery first and monitor before escalating. Do not reboot or shut down the NAS unless explicitly requested. - service: script.send_to_logbook data: topic: "SYNOLOGY" message: >- {{ host_name }} reported a Synology DSM problem for 10 minutes. - Repair {{ issue_id }} opened and Joanna investigation requested. + {% if should_create_repair | bool(false) %} + Repair {{ issue_id }} opened and Joanna investigation requested. + {% else %} + Joanna investigation requested without opening a Repair during the post-outage recovery grace window. + {% endif %} Summary: {{ problem_summary }}. - id: synology_dsm_clear_repair_on_recovery diff --git a/config/script/README.md b/config/script/README.md index 4e42fbb4..6206ae31 100755 --- a/config/script/README.md +++ b/config/script/README.md @@ -39,7 +39,7 @@ Reusable scripts that other automations call for notifications, lighting, safety `script.joanna_dispatch` is the shared handoff contract from Home Assistant automations into Joanna/BearClaw when Home Assistant detects something worth investigating or fixing. Why we use it: -- Keeps one message schema for remediation context (`trigger_context`, `source`, `summary`, `entity_ids`, `diagnostics`, `request`). +- Keeps one message schema for remediation context (`trigger_context`, `source`, `summary`, `entity_ids`, `diagnostics`, `request`, plus optional routing hints). - Avoids repeating direct `rest_command.bearclaw_command` payload formatting in multiple packages. - Lets Home Assistant stay focused on detection, timing, and routing while Joanna acts as the AGENT engineer for infrastructure triage and recommended remediation. - Makes resolution-trigger automations easier to review, update, and audit. @@ -49,6 +49,7 @@ What the helper normalizes before the BearClaw intake call: - `entity_ids` from either a YAML list or a comma-delimited string. - `diagnostics` from either free text or structured mappings/sequences. - `request` guardrails so Joanna defaults to investigation/recommendation, not blind resets or power-cycles. +- `domain_hint`/`lane_hint` default to BearClaw ops routing so HA infrastructure text does not drift into another domain parser. Current automations that kick off automated resolutions (via `script.joanna_dispatch`): | Automation ID | Alias | File | diff --git a/config/script/joanna_dispatch.yaml b/config/script/joanna_dispatch.yaml index 68ff485d..eccc7028 100644 --- a/config/script/joanna_dispatch.yaml +++ b/config/script/joanna_dispatch.yaml @@ -9,6 +9,7 @@ # Notes: Keep this helper generic so package automations can reuse one schema. # Notes: Source defaults to home_assistant_automation.unknown when omitted. # Notes: Automation dispatches are async_only by default so HA calls return quickly while BearClaw works in queue. +# Notes: Automation dispatches default to domain_hint=ops and lane_hint=joanna.ops. # Notes: HA is a dispatcher/integration here; Telegram transport ownership lives in docker_17/codex_appliance. ###################################################################### @@ -31,6 +32,10 @@ joanna_dispatch: description: Extra troubleshooting context. user: description: BearClaw user identity. + domain_hint: + description: BearClaw domain hint. + lane_hint: + description: BearClaw lane hint. sequence: - variables: normalized_context: "{{ trigger_context | default('HA automation', true) }}" @@ -39,6 +44,8 @@ joanna_dispatch: normalized_request: >- {{ request | default('Investigate and recommend remediation. Do not run automated resets or power-cycles unless explicitly requested.', true) }} normalized_user: "{{ user | default('carlo', true) }}" + normalized_domain_hint: "{{ domain_hint | default('ops', true) }}" + normalized_lane_hint: "{{ lane_hint | default('joanna.ops', true) }}" normalized_entity_ids: >- {% if entity_ids is sequence and entity_ids is not string %} {{ entity_ids | map('string') | join(', ') }} @@ -66,4 +73,6 @@ joanna_dispatch: user: "{{ normalized_user }}" source: "{{ normalized_source }}" context: "{{ normalized_context }}" + domain_hint: "{{ normalized_domain_hint }}" + lane_hint: "{{ normalized_lane_hint }}" async_only: true diff --git a/config/templates/speech/briefing.yaml b/config/templates/speech/briefing.yaml index 6040edaf..af398f39 100755 --- a/config/templates/speech/briefing.yaml +++ b/config/templates/speech/briefing.yaml @@ -9,6 +9,8 @@ # Weather, responsibilities, holidays, air quality, and fact prompts parsed by speech_processing/speech_engine. # Notes: Dorm zones are away from Bear Stone; only person state `home` # means someone is physically home at this house. +# Notes: Previous broadcast text is stale context only; current sensor data +# stays authoritative for entry point and action wording. ###################################################################### @@ -88,19 +90,15 @@ {%- endmacro -%} {%- macro window_check() -%} - {% if states.group.entry_points.state != 'off' -%} - {% set comma = joiner(', ') %} - The - {% for state in states.binary_sensor if state.state == 'on' and state.attributes.device_class == 'opening' -%} - {%- endfor %} - {% for group in states.binary_sensor|groupby('state') -%} - {%- for entity in group.list if entity.state == 'on' and entity.attributes.device_class == 'opening' -%} - {{ ' and' if loop.last and not loop.first else comma() }} - {{ entity.attributes.friendly_name }} - {%- endfor -%} - {% endfor %} - need to be closed. - {%- endif -%} + {% set open_entries = states.binary_sensor + | selectattr('state', 'eq', 'on') + | selectattr('attributes.device_class', 'eq', 'opening') + | map(attribute='attributes.friendly_name') + | list %} + {% set entry_count = open_entries | length %} + {% if entry_count > 0 -%} + [Current entry point state: {{ open_entries | join(', ') }} {{ 'is' if entry_count == 1 else 'are' }} still open and {{ 'needs' if entry_count == 1 else 'need' }} to be closed manually. Do not say any physical window or door was closed unless current sensor data says it is closed.] + {%- endif -%} {%- endmacro -%} {%- macro lock_check() -%} @@ -336,6 +334,7 @@ {# call a Random fact about the house or inspiration quote #} {{ ([moon, holiday, days_until ]|random)() }} ] + [Previous broadcast rule: The previous broadcast is stale context only and must not override current Sensor Data. Use it only to avoid repetitive wording. Do not repeat prior claims that a window, door, lock, garage door, or light was changed unless current Sensor Data supports that claim. If current Sensor Data says an entry point is still open and needs closure, say it is still open or needs attention, not that it is closed.] [Previous broadcast for context: "{{ state_attr('sensor.openai_response', 'response') }}" ] {%- endmacro -%}