You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
20 KiB
443 lines
20 KiB
######################################################################
|
|
# @CCOSTAN - Follow Me on X
|
|
# For more info visit https://www.vcloudinfo.com/click-here
|
|
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
|
|
# -------------------------------------------------------------------
|
|
# BearClaw Bridge - Telegram and webhook glue for Joanna agent
|
|
# Routes Telegram -> codex_appliance and codex_appliance -> Telegram/HA.
|
|
# -------------------------------------------------------------------
|
|
# Notes: Keep BearClaw transport + bridge logic centralized in this package.
|
|
# 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: Telegram inline button callbacks are handled here and mapped to BearClaw commands.
|
|
# Notes: Inbound Telegram handling enforces user_id + chat_id allowlists from secrets CSV values.
|
|
# Notes: Reply webhook writes JOANNA activity entries to logbook for traceability.
|
|
# Notes: Status telemetry polling expects !secret bearclaw_status_url (token header stays !secret bearclaw_token).
|
|
# 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
|
|
content_type: application/json
|
|
headers:
|
|
x-codex-token: !secret bearclaw_token
|
|
payload: >
|
|
{
|
|
"text": {{ text | tojson }},
|
|
"user": {{ user | default('carlo') | tojson }},
|
|
"source": {{ source | default('home_assistant') | tojson }},
|
|
"context": {{ context | default(none) | tojson }},
|
|
"callback": {{ callback | default(none) | tojson }}
|
|
}
|
|
|
|
bearclaw_ingest:
|
|
url: !secret bearclaw_ingest_url
|
|
method: post
|
|
content_type: application/json
|
|
headers:
|
|
x-codex-token: !secret bearclaw_token
|
|
payload: >
|
|
{
|
|
"summary": {{ summary | default('event') | tojson }},
|
|
"wake": {{ wake | default(false) | tojson }},
|
|
"source": "home_assistant"
|
|
}
|
|
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
|
|
|
|
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 %}
|
|
automation:
|
|
- id: bearclaw_telegram_bear_command
|
|
alias: BearClaw Telegram Bear Command
|
|
description: Handles /bear commands and forwards text to Joanna.
|
|
mode: queued
|
|
trigger:
|
|
- platform: event
|
|
event_type: telegram_command
|
|
event_data:
|
|
command: /bear
|
|
variables:
|
|
allowed_user_ids_csv: !secret bearclaw_allowed_telegram_user_ids
|
|
allowed_chat_ids_csv: !secret bearclaw_allowed_telegram_chat_ids
|
|
condition:
|
|
- condition: template
|
|
value_template: >-
|
|
{% set has_user = trigger.event.data.user_id is defined %}
|
|
{% set has_chat = trigger.event.data.chat_id is defined %}
|
|
{% set allowed_users = (allowed_user_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set allowed_chats = (allowed_chat_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set incoming_user = trigger.event.data.user_id | default('') | string | trim %}
|
|
{% set incoming_chat = trigger.event.data.chat_id | default('') | string | trim %}
|
|
{{ has_user and has_chat and allowed_users | count > 0 and allowed_chats | count > 0 and incoming_user in allowed_users and incoming_chat in allowed_chats }}
|
|
action:
|
|
- variables:
|
|
command_text: "{{ (trigger.event.data.args | default([])) | join(' ') | trim }}"
|
|
from_user: "{{ (trigger.event.data.from_first | default('carlo')) | lower }}"
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ command_text == '' }}"
|
|
sequence:
|
|
- service: telegram_bot.send_message
|
|
data:
|
|
chat_id: !secret telegram_allowed_chat_id_carlo
|
|
message: "Choose a BearClaw action or send /bear <message>."
|
|
parse_mode: plain_text
|
|
disable_web_page_preview: true
|
|
inline_keyboard:
|
|
- "Status:/bear_status, Add to GitHub:/bear_github_help"
|
|
default:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ command_text }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_command
|
|
|
|
- id: bearclaw_telegram_callback_actions
|
|
alias: BearClaw Telegram Callback Actions
|
|
description: Handles BearClaw Telegram inline button callbacks.
|
|
mode: queued
|
|
trigger:
|
|
- platform: event
|
|
event_type: telegram_callback
|
|
variables:
|
|
allowed_user_ids_csv: !secret bearclaw_allowed_telegram_user_ids
|
|
allowed_chat_ids_csv: !secret bearclaw_allowed_telegram_chat_ids
|
|
condition:
|
|
- condition: template
|
|
value_template: >-
|
|
{% set has_user = trigger.event.data.user_id is defined %}
|
|
{% set has_chat = trigger.event.data.chat_id is defined %}
|
|
{% set allowed_users = (allowed_user_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set allowed_chats = (allowed_chat_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set incoming_user = trigger.event.data.user_id | default('') | string | trim %}
|
|
{% set incoming_chat = trigger.event.data.chat_id | default('') | string | trim %}
|
|
{% set cb = trigger.event.data.data | default('') %}
|
|
{{ has_user and has_chat and allowed_users | count > 0 and allowed_chats | count > 0 and incoming_user in allowed_users and incoming_chat in allowed_chats and (cb.startswith('/bear_') or cb.startswith('/bc_')) }}
|
|
action:
|
|
- variables:
|
|
callback_id: "{{ trigger.event.data.id | default('') }}"
|
|
callback_data: "{{ trigger.event.data.data | default('') | trim }}"
|
|
from_user: "{{ (trigger.event.data.from_first | default('carlo')) | lower }}"
|
|
callback_payload: "{{ callback_data[6:] if callback_data.startswith('/bear_') else '' }}"
|
|
callback_parts: "{{ callback_payload.split('_') if callback_payload | length > 0 else [] }}"
|
|
action_name: "{{ callback_parts[0] if callback_parts | count > 0 else '' }}"
|
|
job_id: "{{ callback_parts[1:] | join('_') if callback_parts | count > 1 else '' }}"
|
|
- service: telegram_bot.answer_callback_query
|
|
continue_on_error: true
|
|
data:
|
|
callback_query_id: "{{ callback_id }}"
|
|
message: "Processing..."
|
|
show_alert: false
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ callback_data.startswith('/bc_') }}"
|
|
sequence:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: ""
|
|
user: "{{ from_user }}"
|
|
source: telegram_callback
|
|
callback:
|
|
token: "{{ callback_data[4:] }}"
|
|
raw: "{{ callback_data }}"
|
|
chat_id: "{{ trigger.event.data.chat_id | default('') }}"
|
|
callback_id: "{{ callback_id }}"
|
|
message_id: "{{ trigger.event.data.message.message_id if trigger.event.data.message is defined and trigger.event.data.message.message_id is defined else '' }}"
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ action_name == 'status' }}"
|
|
sequence:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ 'status ' ~ job_id if job_id | length > 0 else 'status' }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_callback
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ action_name == 'cancel' and job_id | length > 0 }}"
|
|
sequence:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ 'cancel ' ~ job_id }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_callback
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ action_name == 'github_help' }}"
|
|
sequence:
|
|
- service: script.joanna_send_telegram
|
|
data:
|
|
message: >-
|
|
To create a GitHub capture, send:
|
|
add to github <short topic>
|
|
Example: add to github Evaluate Smart Home Planner HACS app
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ action_name == 'yes' }}"
|
|
sequence:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ 'yes ' ~ job_id if job_id | length > 0 else 'Yes.' }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_callback
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ action_name == 'no' }}"
|
|
sequence:
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ 'no ' ~ job_id if job_id | length > 0 else 'No.' }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_callback
|
|
default:
|
|
- service: script.joanna_send_telegram
|
|
data:
|
|
message: "Unknown BearClaw button action. Try /bear to open the menu."
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.event.data.message is defined and trigger.event.data.message.message_id is defined }}"
|
|
sequence:
|
|
- service: telegram_bot.edit_replymarkup
|
|
continue_on_error: true
|
|
data:
|
|
chat_id: "{{ trigger.event.data.chat_id }}"
|
|
message_id: "{{ trigger.event.data.message.message_id }}"
|
|
inline_keyboard: []
|
|
|
|
- id: bearclaw_telegram_text_no_slash_needed
|
|
alias: BearClaw Telegram Text No Slash Needed
|
|
description: Treats plain Telegram text as BearClaw command input.
|
|
mode: queued
|
|
trigger:
|
|
- platform: event
|
|
event_type: telegram_text
|
|
variables:
|
|
allowed_user_ids_csv: !secret bearclaw_allowed_telegram_user_ids
|
|
allowed_chat_ids_csv: !secret bearclaw_allowed_telegram_chat_ids
|
|
condition:
|
|
- condition: template
|
|
value_template: >-
|
|
{% set has_user = trigger.event.data.user_id is defined %}
|
|
{% set has_chat = trigger.event.data.chat_id is defined %}
|
|
{% set allowed_users = (allowed_user_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set allowed_chats = (allowed_chat_ids_csv | default('', true) | string).split(',') | map('trim') | reject('equalto', '') | list %}
|
|
{% set incoming_user = trigger.event.data.user_id | default('') | string | trim %}
|
|
{% set incoming_chat = trigger.event.data.chat_id | default('') | string | trim %}
|
|
{% set plain_text = trigger.event.data.text | default('') | trim %}
|
|
{{ has_user and has_chat and allowed_users | count > 0 and allowed_chats | count > 0 and incoming_user in allowed_users and incoming_chat in allowed_chats and plain_text != '' and not plain_text.startswith('/') }}
|
|
action:
|
|
- variables:
|
|
plain_text: "{{ trigger.event.data.text | default('') | trim }}"
|
|
from_user: "{{ (trigger.event.data.from_first | default('carlo')) | lower }}"
|
|
- service: rest_command.bearclaw_command
|
|
data:
|
|
text: "{{ plain_text }}"
|
|
user: "{{ from_user }}"
|
|
source: telegram_text
|
|
|
|
- id: bearclaw_reply_webhook
|
|
alias: BearClaw Reply Webhook
|
|
description: Receives BearClaw replies from codex_appliance and relays to Telegram/HA push.
|
|
mode: queued
|
|
trigger:
|
|
- platform: webhook
|
|
webhook_id: !secret bearclaw_reply_webhook_id
|
|
allowed_methods:
|
|
- POST
|
|
local_only: true
|
|
action:
|
|
- variables:
|
|
message: "{{ trigger.json.message | default('Joanna: empty reply') }}"
|
|
telegram_message: "{{ trigger.json.telegram_message | default(message, true) }}"
|
|
telegram_parse_mode: >-
|
|
{% set raw = trigger.json.telegram_parse_mode | default('plain_text', true) | string | lower | trim %}
|
|
{% if raw in ['html', 'plain_text'] %}
|
|
{{ raw }}
|
|
{% else %}
|
|
plain_text
|
|
{% endif %}
|
|
telegram_disable_preview: "{{ trigger.json.disable_web_page_preview | default(true, true) }}"
|
|
level: "{{ trigger.json.level | default('active') | lower }}"
|
|
inline_keyboard_payload: >-
|
|
{% set kb = trigger.json.inline_keyboard if trigger.json.inline_keyboard is defined else none %}
|
|
{% if kb is string %}
|
|
{{ kb | trim }}
|
|
{% elif kb is sequence and kb is not string and (kb | count) > 0 %}
|
|
{{ kb | map('string') | map('trim') | reject('equalto', '') | list | join('\n') }}
|
|
{% else %}
|
|
{{ '' }}
|
|
{% endif %}
|
|
logbook_message: >-
|
|
{% set compact = message | replace('\r', ' ') | replace('\n', ' ') | trim %}
|
|
{% if compact | length > 240 %}
|
|
{{ compact[:237] ~ '...' }}
|
|
{% else %}
|
|
{{ compact }}
|
|
{% endif %}
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ inline_keyboard_payload | length > 0 }}"
|
|
sequence:
|
|
- service: telegram_bot.send_message
|
|
data:
|
|
chat_id: !secret telegram_allowed_chat_id_carlo
|
|
message: "{{ telegram_message }}"
|
|
parse_mode: "{{ telegram_parse_mode }}"
|
|
disable_web_page_preview: "{{ telegram_disable_preview }}"
|
|
inline_keyboard: "{{ inline_keyboard_payload }}"
|
|
default:
|
|
- service: script.joanna_send_telegram
|
|
data:
|
|
message: "{{ telegram_message }}"
|
|
parse_mode: "{{ telegram_parse_mode }}"
|
|
disable_web_page_preview: "{{ telegram_disable_preview }}"
|
|
- service: script.send_to_logbook
|
|
data:
|
|
topic: JOANNA
|
|
message: "{{ level | upper }}: {{ logbook_message }}"
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ level in ['warning', 'error', 'critical'] }}"
|
|
sequence:
|
|
- service: script.notify_engine
|
|
data:
|
|
title: Joanna Alert
|
|
value1: "{{ message }}"
|