mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2026-07-05 05:37:03 -07:00
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.
This commit is contained in:
+1
-1
@@ -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:
|
||||
|
||||
+62
-23
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user