Update HA version to 2026.4.0, enhance BearClaw integration with async command payload support, improve vacuum area mapping for cleaning segments, and ensure async dispatch for Joanna automation.

pull/1702/head
Carlo Costanzo 2 months ago
parent 88e791e23c
commit c26c8bc64d

@ -1 +1 @@
2026.3.4
2026.4.0

@ -15,6 +15,7 @@
# 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: Telegram freeform input now includes LLM-first routing context to improve intent understanding before entity lookups.
# 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/
######################################################################
@ -22,6 +23,7 @@ rest_command:
bearclaw_command:
url: !secret bearclaw_command_url
method: post
timeout: 30
content_type: application/json
headers:
x-codex-token: !secret bearclaw_token
@ -31,7 +33,8 @@ rest_command:
"user": {{ user | default('carlo') | tojson }},
"source": {{ source | default('home_assistant') | tojson }},
"context": {{ context | default(none) | tojson }},
"callback": {{ callback | default(none) | tojson }}
"callback": {{ callback | default(none) | tojson }},
"async_only": {{ async_only | default(false) | tojson }}
}
bearclaw_ingest:

@ -275,10 +275,14 @@ template:
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0
and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', ''])
or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state') | count > 0
and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state_2') | count > 0
and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in ns.items %}
@ -295,10 +299,14 @@ template:
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0
and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', ''])
or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state') | count > 0
and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state_2') | count > 0
and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in ns.items %}
@ -316,10 +324,14 @@ template:
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0
and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', ''])
or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state') | count > 0
and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state_2') | count > 0
and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in discovered_ns.items %}
@ -343,10 +355,14 @@ template:
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0
and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', ''])
or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state') | count > 0
and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', ''])
or (expand('sensor.' ~ key ~ '_state_2') | count > 0
and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in discovered_ns.items %}
@ -730,7 +746,13 @@ script:
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.
Reply with explicit status fields:
resolved=true/false,
root_cause,
action_taken,
verification (entity plus observed state),
next_action_required=true/false.
Use Duplicati and related host/container telemetry to verify recovery.
- conditions: "{{ op == 'clear' }}"
sequence:
- variables:

@ -12,7 +12,7 @@
# - Treat 2+ minutes in a room as "being cleaned" and dequeue immediately (queue = remaining rooms).
# - Phase changes happen only after verified completion at dock (`task_status: completed`).
# - Guarded fallback: if docked with empty queue for 10 minutes but no `completed`, advance with `fallback_advance` log.
# - Avoid reissuing `dreame_vacuum.vacuum_clean_segment` while already cleaning; only send a new segment job when starting/resuming or switching phases.
# - Use `vacuum.clean_area` (HA 2026.3+) and keep room->area mappings aligned with Home Assistant Areas.
# - Jinja2 loop scoping: use a `namespace` when building lists (otherwise the queue can appear empty and get cleared).
# - If docked+completed still has queue entries, treat queue as stale and clear it before phase advance.
# - Mop phases use `sweeping_and_mopping` instead of mop-only.
@ -133,6 +133,30 @@ script:
{{ bath_ids }}
{% endif %}
segments_to_clean: "{{ queue_ints if queue_ints | length > 0 else phase_segments }}"
segment_area_name_map:
14: Kitchen
12: "Dining Room"
10: "Living Room"
7: "Master Bedroom"
15: Foyer
9: "Stacey Office"
13: Hallway
8: "Justin Bedroom"
6: "Paige Bedroom"
4: "Master Bathroom"
2: Office
1: "Pool Bath"
3: "Kids Bathroom"
cleaning_area_ids: >
{% set ns = namespace(ids=[]) %}
{% for seg in segments_to_clean %}
{% set area_name = segment_area_name_map.get(seg) %}
{% set aid = area_id(area_name) if area_name else none %}
{% if aid %}
{% set ns.ids = ns.ids + [aid] %}
{% endif %}
{% endfor %}
{{ ns.ids }}
# 0. Reseed the current phase when queue is empty.
- choose:
@ -168,6 +192,19 @@ script:
- stop: 'No rooms left to clean today.'
default: []
# 2b. Clean-area needs a mapped Home Assistant area ID for every segment
- choose:
- conditions:
- condition: template
value_template: "{{ cleaning_area_ids | length != segments_to_clean | length }}"
sequence:
- service: script.send_to_logbook
data:
topic: "VACUUM"
message: "Missing area mappings for one or more segments {{ segments_to_clean }}; skipping clean_area."
- stop: "Incomplete Home Assistant area mappings."
default: []
# 3. Start cleaning (but don't clobber an active job)
- choose:
- conditions:
@ -177,7 +214,7 @@ script:
- service: script.send_to_logbook
data:
topic: "VACUUM"
message: "Vacuum is already cleaning; queue/phase updated but not issuing a new segment job."
message: "Vacuum is already cleaning; queue/phase updated but not issuing a new clean_area action."
- stop: "Already cleaning."
default: []
@ -192,12 +229,12 @@ script:
entity_id: vacuum.l10s_vacuum
data:
fan_speed: Standard
- service: dreame_vacuum.vacuum_clean_segment
- service: vacuum.clean_area
target:
entity_id: vacuum.l10s_vacuum
data:
# Clean the non-bathrooms if any, otherwise clean the bathrooms
segments: "{{ segments_to_clean }}"
# Clean mapped Home Assistant areas for this phase queue.
cleaning_area_id: "{{ cleaning_area_ids }}"
## 3. Automations
@ -294,22 +331,24 @@ automation:
id: kids_bathroom
variables:
room_map:
kitchen: {segment: 14, name: Kitchen}
dining_room: {segment: 12, name: 'Dining Room'}
living_room: {segment: 10, name: 'Living Room'}
master_bedroom: {segment: 7, name: 'Master Bedroom'}
foyer: {segment: 15, name: Foyer}
stacey_office: {segment: 9, name: 'Stacey Office'}
formal_dining: {segment: 17, name: 'Formal Dining'}
hallway: {segment: 13, name: Hallway}
justin_bedroom: {segment: 8, name: 'Justin Bedroom'}
paige_bedroom: {segment: 6, name: 'Paige Bedroom'}
master_bathroom: {segment: 4, name: 'Master Bathroom'}
office: {segment: 2, name: Office}
pool_bath: {segment: 1, name: 'Pool Bath'}
kids_bathroom: {segment: 3, name: 'Kids Bathroom'}
kitchen: {segment: 14, name: Kitchen, area: Kitchen}
dining_room: {segment: 12, name: 'Dining Room', area: 'Dining Room'}
living_room: {segment: 10, name: 'Living Room', area: 'Living Room'}
master_bedroom: {segment: 7, name: 'Master Bedroom', area: 'Master Bedroom'}
foyer: {segment: 15, name: Foyer, area: Foyer}
stacey_office: {segment: 9, name: 'Stacey Office', area: 'Stacey Office'}
formal_dining: {segment: 17, name: 'Formal Dining', area: 'Formal Dining'}
hallway: {segment: 13, name: Hallway, area: Hallway}
justin_bedroom: {segment: 8, name: 'Justin Bedroom', area: 'Justin Bedroom'}
paige_bedroom: {segment: 6, name: 'Paige Bedroom', area: 'Paige Bedroom'}
master_bathroom: {segment: 4, name: 'Master Bathroom', area: 'Master Bathroom'}
office: {segment: 2, name: Office, area: Office}
pool_bath: {segment: 1, name: 'Pool Bath', area: 'Pool Bath'}
kids_bathroom: {segment: 3, name: 'Kids Bathroom', area: 'Kids Bathroom'}
room_key: "{{ trigger.id }}"
room_name: "{{ room_map[room_key].name }}"
area_name: "{{ room_map[room_key].area }}"
area_id_value: "{{ area_id(area_name) if area_name else none }}"
segment_id: "{{ room_map[room_key].segment | int }}"
vac_state: "{{ states('vacuum.l10s_vacuum') }}"
on_demand: "{{ is_state('input_boolean.l10s_vacuum_on_demand', 'on') }}"
@ -319,7 +358,7 @@ automation:
- choose:
- conditions:
- condition: template
value_template: "{{ can_start }}"
value_template: "{{ can_start and area_id_value is not none }}"
sequence:
- service: script.send_to_logbook
data:
@ -338,17 +377,17 @@ automation:
data:
fan_speed: Standard
- continue_on_error: true
service: dreame_vacuum.vacuum_clean_segment
service: vacuum.clean_area
target:
entity_id: vacuum.l10s_vacuum
data:
segments: "{{ [segment_id] }}"
cleaning_area_id: "{{ [area_id_value] }}"
- delay: "00:00:02"
default:
- service: script.send_to_logbook
data:
topic: "VACUUM"
message: "One-off clean blocked: {{ room_name }} (vac={{ vac_state }}, on_demand={{ on_demand }}, queue='{{ queue_raw }}')."
message: "One-off clean blocked: {{ room_name }} (area={{ area_name }}, area_id={{ area_id_value }}, vac={{ vac_state }}, on_demand={{ on_demand }}, queue='{{ queue_raw }}')."
- service: input_boolean.turn_off
data:
entity_id: "{{ trigger.entity_id }}"

@ -8,6 +8,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.
######################################################################
joanna_dispatch:
@ -64,3 +65,4 @@ joanna_dispatch:
user: "{{ normalized_user }}"
source: "{{ normalized_source }}"
context: "{{ normalized_context }}"
async_only: true

Loading…
Cancel
Save

Powered by TurnKey Linux.