###################################################################### # @CCOSTAN - Follow Me on X # For more info visit https://www.vcloudinfo.com/click-here # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # BearClaw Integration - Home Assistant dispatch + lifecycle callbacks # Home Assistant dispatches jobs to codex_appliance and receives structured lifecycle callbacks. # ------------------------------------------------------------------- # Notes: Telegram ingress now lives directly in docker_17/codex_appliance. # Notes: Most BearClaw decision logic runs in docker_17/codex_appliance (server.js). # Notes: GitHub capture behavior (issue creation/labels/research flow) belongs in codex_appliance, not HA YAML. # Notes: Shared script helper `script.joanna_dispatch` lives in config/script/joanna_dispatch.yaml. # Notes: The callback webhook writes JOANNA activity entries to logbook for traceability and optional HA alerts. # Notes: Status telemetry polling expects !secret bearclaw_status_url (token header stays !secret bearclaw_token). # Notes: Nightly Duplicati verification calls a codex_appliance admin endpoint and returns structured health to HA via response_variable. # 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: Blog: https://www.vcloudinfo.com/2026/03/joanna-dispatch-telemetry-home-assistant-infrastructure-dashboard/ ###################################################################### rest_command: bearclaw_command: url: !secret bearclaw_command_url method: post timeout: 30 content_type: application/json headers: x-codex-token: !secret bearclaw_token payload: > { "kind": "command", "transport": "ha", "source": {{ source | default('home_assistant') | tojson }}, "actor": { "id": {{ user | default('carlo') | tojson }} }, "conversation": { "id": {{ source | default('home_assistant') | tojson }}, "type": "automation" }, "input": { "text": {{ text | tojson }}, "context": {{ context | default(none) | tojson }}, "callback": {{ callback | default(none) | tojson }} }, "replyTargets": [ { "type": "ha", "callbackEventType": "lifecycle" } ], "priority": {{ priority | default(none) | tojson }}, "async_only": {{ async_only | default(false) | tojson }} } bearclaw_ingest: url: !secret bearclaw_ingest_url method: post content_type: application/json headers: x-codex-token: !secret bearclaw_token payload: > { "kind": "event", "transport": "ha", "source": "homeassistant", "actor": { "id": "system" }, "conversation": { "id": "homeassistant", "type": "automation" }, "input": { "text": {{ summary | default('event') | tojson }}, "event": { "summary": {{ summary | default('event') | tojson }}, "wake": {{ wake | default(false) | tojson }}, "priority": {{ priority | default(none) | tojson }}, "source": "homeassistant" } }, "replyTargets": [] } bearclaw_duplicati_verify: url: !secret bearclaw_duplicati_verify_url method: post timeout: 60 content_type: application/json headers: x-codex-token: !secret bearclaw_token payload: > { "reason": {{ reason | default('home_assistant') | tojson }} } bearclaw_telegram_send: url: !secret bearclaw_telegram_send_url method: post timeout: 30 content_type: application/json headers: x-codex-token: !secret bearclaw_token payload: > { "message": {{ message | tojson }}, "parse_mode": {{ parse_mode | default('plain_text') | tojson }}, "disable_web_page_preview": {{ disable_web_page_preview | default(true) | tojson }}, "chat_id": {{ chat_id | default(none) | tojson }}, "user": {{ user | default('carlo') | tojson }} } sensor: - platform: rest name: BearClaw Status Telemetry unique_id: bearclaw_status_telemetry resource: !secret bearclaw_status_url method: GET scan_interval: 60 timeout: 30 headers: x-codex-token: !secret bearclaw_token value_template: "{{ 'ok' if value_json.ok | default(false) else 'error' }}" json_attributes: - dispatchStats - queue - active - platform - transports - qmdHealth - memoryIndex template: - sensor: - name: Joanna HA Dispatches 24h unique_id: joanna_ha_dispatches_24h icon: mdi:calendar-clock unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('h24', {}).get('haAutomation', {}).get('dispatched', 0) | int(0) }} - name: Joanna HA Dispatches 7d unique_id: joanna_ha_dispatches_7d icon: mdi:calendar-range unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('d7', {}).get('haAutomation', {}).get('dispatched', 0) | int(0) }} - name: Joanna HA Dispatches 30d unique_id: joanna_ha_dispatches_30d icon: mdi:calendar-month unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('d30', {}).get('haAutomation', {}).get('dispatched', 0) | int(0) }} - name: Joanna HA Completed 24h unique_id: joanna_ha_completed_24h icon: mdi:check-decagram unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('h24', {}).get('haAutomation', {}).get('completed', 0) | int(0) }} - name: Joanna HA Success Rate 24h unique_id: joanna_ha_success_rate_24h icon: mdi:percent-circle unit_of_measurement: '%' state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('h24', {}).get('haAutomation', {}).get('successRatePct', 0) | float(0) }} - name: Joanna HA Errors 24h unique_id: joanna_ha_errors_24h icon: mdi:alert-circle-outline unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {% set outcomes = stats.get('windows', {}).get('h24', {}).get('haAutomation', {}).get('outcomes', {}) %} {{ (outcomes.get('error', 0) | int(0)) + (outcomes.get('notify_error', 0) | int(0)) }} - name: Joanna All Dispatches 24h unique_id: joanna_all_dispatches_24h icon: mdi:calendar-clock unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('h24', {}).get('all', {}).get('dispatched', 0) | int(0) }} - name: Joanna All Dispatches 7d unique_id: joanna_all_dispatches_7d icon: mdi:calendar-range unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('d7', {}).get('all', {}).get('dispatched', 0) | int(0) }} - name: Joanna All Dispatches 30d unique_id: joanna_all_dispatches_30d icon: mdi:calendar-month unit_of_measurement: jobs state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {{ stats.get('windows', {}).get('d30', {}).get('all', {}).get('dispatched', 0) | int(0) }} - name: Joanna Queue Pending unique_id: joanna_queue_pending icon: mdi:playlist-clock unit_of_measurement: jobs state_class: measurement state: >- {% set queue = state_attr('sensor.bearclaw_status_telemetry', 'queue') | default({}, true) %} {{ queue.get('pending', 0) | int(0) }} - name: Joanna Active Job Minutes unique_id: joanna_active_job_minutes icon: mdi:timer-outline unit_of_measurement: min state_class: measurement state: >- {% set active = state_attr('sensor.bearclaw_status_telemetry', 'active') | default({}, true) %} {% set elapsed_ms = active.get('elapsedMs', 0) | int(0) %} {{ (elapsed_ms / 60000) | round(1) }} - name: Joanna Minutes Since HA Dispatch unique_id: joanna_minutes_since_ha_dispatch icon: mdi:clock-fast unit_of_measurement: min state_class: measurement state: >- {% set stats = state_attr('sensor.bearclaw_status_telemetry', 'dispatchStats') | default({}, true) %} {% set dispatched_at = stats.get('latest', {}).get('haAutomation', {}).get('dispatchedAt') %} {% set dispatched_ts = as_timestamp(dispatched_at, 0) %} {% if dispatched_ts > 0 %} {{ ((as_timestamp(now()) - dispatched_ts) / 60) | round(1) }} {% else %} 0 {% endif %} - name: Joanna QMD Consecutive Failures unique_id: joanna_qmd_consecutive_failures icon: mdi:heart-pulse unit_of_measurement: checks state_class: measurement state: >- {% set qmd = state_attr('sensor.bearclaw_status_telemetry', 'qmdHealth') | default({}, true) %} {{ qmd.get('consecutiveFailures', 0) | int(0) }} - name: Joanna Memory Docs unique_id: joanna_memory_docs icon: mdi:file-document-multiple-outline unit_of_measurement: docs state_class: measurement state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {{ memory.get('docs', 0) | int(0) }} - name: Joanna Memory Chunks unique_id: joanna_memory_chunks icon: mdi:text-box-search-outline unit_of_measurement: chunks state_class: measurement state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {{ memory.get('chunks', 0) | int(0) }} - name: Joanna Minutes Since Memory Index unique_id: joanna_minutes_since_memory_index icon: mdi:database-clock-outline unit_of_measurement: min state_class: measurement state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {% set indexed_at = memory.get('indexedAt') %} {% set indexed_ts = as_timestamp(indexed_at, 0) %} {% if indexed_ts > 0 %} {{ ((as_timestamp(now()) - indexed_ts) / 60) | round(1) }} {% else %} 0 {% endif %} - binary_sensor: - name: Joanna QMD Healthy unique_id: joanna_qmd_healthy icon: mdi:heart-pulse state: >- {% set qmd = state_attr('sensor.bearclaw_status_telemetry', 'qmdHealth') | default({}, true) %} {{ qmd.get('healthy', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }} - name: Joanna Session Source Indexed unique_id: joanna_session_source_indexed icon: mdi:message-text-clock-outline state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {% set options = memory.get('options', {}) if memory is mapping else {} %} {{ options.get('includeSessionSource', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }} - name: Joanna Memory Index Stale unique_id: joanna_memory_index_stale icon: mdi:database-alert-outline state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {{ memory.get('stale', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }} automation: - id: bearclaw_lifecycle_webhook alias: BearClaw Lifecycle Webhook description: Receives structured BearClaw lifecycle callbacks for HA-triggered work. mode: queued trigger: - platform: webhook webhook_id: !secret bearclaw_reply_webhook_id allowed_methods: - POST local_only: true action: - variables: event_type: "{{ trigger.json.event_type | default('progress') | lower }}" status: "{{ trigger.json.status | default(event_type, true) | lower }}" source: "{{ trigger.json.source | default('homeassistant', true) | string | lower | trim }}" severity: "{{ trigger.json.severity | default('active', true) | string | lower | trim }}" summary: "{{ trigger.json.summary | default('Joanna lifecycle callback', true) }}" message: "{{ trigger.json.message | default(summary, true) }}" job_id: "{{ trigger.json.job_id | default('', true) }}" run_id: "{{ trigger.json.run_id | default('', true) }}" logbook_message: >- {% set compact = (event_type ~ ' | ' ~ summary ~ ' | ' ~ message) | replace('\r', ' ') | replace('\n', ' ') | trim %} {% if compact | length > 240 %} {{ compact[:237] ~ '...' }} {% else %} {{ compact }} {% endif %} - service: script.send_to_logbook data: topic: JOANNA message: "{{ status | upper }}: {{ logbook_message }}" - choose: - conditions: - condition: template value_template: "{{ severity in ['warning', 'error', 'critical'] or status in ['failed', 'canceled'] }}" sequence: - service: script.notify_engine data: title: Joanna Alert value1: >- {{ summary }} {% if job_id | trim != '' %} (job={{ job_id }}) {% endif %} {% if run_id | trim != '' %} run={{ run_id }} {% endif %} {{ '\n' ~ message if message | trim != '' else '' }}