16 KiB
ADR-129: HOMECORE-AUTO — Automation engine, script runner, and template evaluator
| Field | Value |
|---|---|
| Status | Proposed |
| Date | 2026-05-25 |
| Deciders | ruv |
| Codename | HOMECORE-AUTO |
| Relates to | ADR-126 (HOMECORE master), ADR-127 (HOMECORE-CORE), ADR-129 implicit, ADR-133 (HOMECORE-ASSIST) |
| Tracking issue | TBD |
1. Context
Home Assistant's automation system is defined across three components:
-
homeassistant/components/automation/__init__.py— the automation manager: loads automation YAML, evaluates trigger platforms, calls the script executor when conditions pass. The core class isAutomationEntitywhich extendsToggleEntity. Automations are themselves HA entities withstate = on/off. -
homeassistant/components/script/__init__.py— the script executor: a sequence of actions (service calls, conditions, delays, events, template variables,choose,parallel,repeat,wait_for_trigger). Scripts are entities too (ScriptEntityextendsToggleEntity). The execution engine supports five run modes:single,restart,queued,parallel,ignore_first. -
homeassistant/helpers/template.py— HA's Jinja2 customisation layer: wraps the upstreamjinja2Python library with HA-specific globals (states(),is_state(),state_attr(),now(),utcnow(),as_timestamp(),distance(),closest(), etc.), custom filters (regex_match,round,timestamp_local), and a sandboxedEnvironmentthat prevents file I/O and dangerous evaluations.
1.1 Scale and surface
HA's automation YAML supports:
- 17 trigger platforms (state, time, numeric_state, template, event, homeassistant, zone, geo_location, device, calendar, conversation, mqtt, webhook, tag, sun, time_pattern, persistent_notification)
- 7 condition types (state, numeric_state, time, template, zone, sun, device)
- 22+ action types (call_service, delay, wait_template, fire_event, device_action, choose, if, parallel, repeat, sequence, stop, set_conversation_response, ...)
The YAML schema is validated by voluptuous schemas defined in homeassistant/helpers/config_validation.py (~5,000 lines).
1.2 Jinja2 is the critical surface
HA templates are used not only in automations but in dashboard cards, notification messages, and script variables. The HA frontend sends template strings to the API's POST /api/template endpoint for server-side evaluation. Any HOMECORE instance that claims API compatibility must execute Jinja2-compatible templates or existing automations will break.
Full Jinja2 support in Rust without Python is non-trivial. The approach chosen here uses a WASM-compiled MiniJinja (the minijinja Rust crate compiled with HA-specific extension functions) rather than a full Python Jinja2 re-implementation.
2. Decision
Build the homecore-automation crate with three components:
- YAML parser:
serde_yaml+ custom validator that parses HA's automation and script YAML into typed Rust structs. Validates trigger, condition, and action schemas at load time. - Trigger evaluator: a Tokio task per loaded automation that subscribes to the HOMECORE event bus (ADR-127) and evaluates trigger conditions in Rust. When a trigger fires and conditions pass, it enqueues the automation action sequence.
- Action executor: a script runner that processes action sequences. Service calls go to the HOMECORE service registry. Delays use
tokio::time::sleep. Template evaluation uses MiniJinja. Complex conditions (optional) can route to a ruflo agent (ADR-133).
2.1 Template evaluator: MiniJinja + HA-compatible extension functions
minijinja (crates.io version 2.x) is a production-quality Jinja2 implementation in pure Rust. It is missing 5–10% of Jinja2's surface area (notably: {% block %} / {% extends %} template inheritance, and some Jinja2 Python-specific filters), but covers 100% of HA's automation template usage.
HA-specific globals added on top of MiniJinja:
env.add_global("states", minijinja::Value::from_function(ha_states_global));
env.add_global("is_state", minijinja::Value::from_function(ha_is_state_global));
env.add_global("state_attr", minijinja::Value::from_function(ha_state_attr_global));
env.add_global("now", minijinja::Value::from_function(ha_now_global));
env.add_global("utcnow", minijinja::Value::from_function(ha_utcnow_global));
env.add_global("as_timestamp", minijinja::Value::from_function(ha_as_timestamp_global));
env.add_global("distance", minijinja::Value::from_function(ha_distance_global));
env.add_global("iif", minijinja::Value::from_function(ha_iif_global));
Each global function reads from the HOMECORE state machine (ADR-127) via an Arc<StateMachine> captured at environment construction time. Template evaluation is synchronous (MiniJinja is sync) but runs in a tokio::task::spawn_blocking wrapper to avoid blocking the async executor.
2.2 WASM evaluator for untrusted template strings
Dashboard card templates submitted via POST /api/template come from user-authored YAML, not first-party code. HA evaluates these in the same Python process, relying on Jinja2's SandboxedEnvironment for safety. HOMECORE uses a WASM-sandboxed MiniJinja evaluator:
- A single WASM module (
homecore-template-eval.wasm) is compiled from the MiniJinja crate with the HA extension globals stubbed to call host functions. - Template strings are passed into the WASM module via the HOMECORE plugin ABI (ADR-128 §5.1).
- The WASM sandbox prevents file I/O, network access, and infinite loops (via Wasmtime fuel metering — 100,000 instructions per template evaluation).
- Result is returned as a string to the HOMECORE API.
This is the same Wasmtime host already used for integration plugins (ADR-128) — no additional WASM runtime dependency.
3. HA-side reference table
| HA module / file | What it does | HOMECORE preserves | Changes | Drops |
|---|---|---|---|---|
automation/__init__.py AutomationEntity |
Automation as a toggle entity (on/off) with triggers/conditions/actions | Automation is a HOMECORE entity with same on/off state semantics | Rust struct AutomationEntity implementing HomeCoreEntity trait |
Python class hierarchy, voluptuous schema |
automation/__init__.py TriggerActionConfig |
Trigger → condition → action pipeline | Full trigger/condition/action pipeline | Typed Rust enums per trigger platform | Python dict-based config |
automation/trigger.py |
Delegates to per-platform trigger modules (homeassistant/components/<platform>/trigger.py) |
Same per-platform dispatch | Rust match arm per trigger type | Python dynamic module import |
script/__init__.py Script |
Script entity + action sequence executor | Same 22 action types | Rust enum Action with all variants |
Python asyncio coroutines |
script/__init__.py run modes |
single, restart, queued, parallel, ignore_first |
All 5 run modes | Tokio-based concurrency control (semaphore for queued, parallel) |
Python asyncio task management |
helpers/template.py Template |
Jinja2 evaluation + HA globals | Same HA global function names and signatures | MiniJinja instead of Python Jinja2; WASM sandbox for user templates | Python jinja2 library; voluptuous coercions in templates |
helpers/config_validation.py |
cv.template, cv.entity_id, time period validators |
Same validation semantics | Rust custom deserializers implementing serde::Deserialize |
voluptuous; Python regex |
components/automation/blueprint.py |
Blueprint system (reusable automation templates with input variables) | Blueprint YAML schema + variable substitution | Pure Rust YAML substitution | Python Blueprint class hierarchy |
4. Public API parity table
| HA automation surface | HOMECORE equivalent |
|---|---|
automation.trigger (state, time, numeric_state, template, event, ...) |
Trigger enum with variants for all 17 HA trigger platforms |
automation.condition (state, numeric_state, time, template, zone, sun, device) |
Condition enum with variants for all 7 condition types |
automation.action — call_service, delay, fire_event, choose, if, parallel, repeat, wait_template, stop |
Action enum with variants for all 22 action types |
script.run_mode — single, restart, queued, parallel |
RunMode enum with 5 variants |
POST /api/template (REST eval of a template string) |
Same endpoint in HOMECORE-API (ADR-130); backed by WASM-sandboxed MiniJinja |
| Automation entity: `state = on | off, attributes.last_triggered, attributes.id` |
automation.trigger service (manually trigger an automation) |
homecore.automation.trigger service; same service call data schema |
automation.reload service (reload automations.yaml) |
homecore.automation.reload service |
automation.toggle service |
Standard HomeCoreEntity toggle service |
Blueprint YAML with blueprint: key and input: variables |
Blueprint parsed by HOMECORE YAML parser; same substitution semantics |
5. Trigger platform mapping
| HA trigger platform | HOMECORE implementation |
|---|---|
state |
Subscribe to state_changed broadcast; match entity_id, from, to, for |
numeric_state |
Subscribe to state_changed; parse state as f64; compare against above/below |
time |
tokio::time::sleep_until to next occurrence; re-arm after fire |
time_pattern |
Cron-style evaluation using cron crate; tokio timer task |
template |
Re-evaluate template on every state_changed; fire when template transitions from false to true |
event |
Subscribe to named domain event on event bus |
homeassistant (start/stop) |
Subscribe to HomeAssistantStart / HomeAssistantStop typed events |
zone |
Subscribe to zone.entered / zone.left events from the device tracker integration |
mqtt |
Subscribe to MQTT topic via the MQTT plugin (ADR-128); fire event when message arrives |
webhook |
HOMECORE-API registers a webhook path; fires event on POST |
calendar |
Subscribe to calendar event from calendar integration |
conversation |
Subscribe to conversation.user_input event; match intent/sentence |
geo_location |
Subscribe to geo_location.entered / geo_location.left |
sun |
Compute sunrise/sunset from latitude/longitude in homecore.config; tokio timer |
device |
Delegate to integration-specific device trigger via WASM plugin |
persistent_notification |
Subscribe to persistent_notification.create event |
tag |
Subscribe to tag.scanned event from NFC/QR integration |
6. Phased implementation plan
P1 — YAML parser (2 weeks)
- Define Rust enums for
Trigger,Condition,Action,RunModewithserdedeserialization. - Parse an existing
automations.yamlfrom a real HA install with zero errors (test fixture). - Validator: reject unknown trigger platforms with a clear error message.
- Unit tests: parse 50 automation fixtures covering all 17 trigger types and 22 action types.
P2 — State and event triggers (2 weeks)
- Implement
state,numeric_state,event,homeassistant,time,time_patterntrigger evaluators. ConditionEvaluatorforstate,numeric_state,timeconditions.ActionExecutorforcall_service,delay,fire_event,stopaction types.- Integration test: load one automation (state trigger → call_service action); verify fires correctly when state changes.
P3 — Full action set + MiniJinja (3 weeks)
- MiniJinja + HA extension globals;
POST /api/templateendpoint wired to WASM evaluator. templatetrigger +templatecondition evaluators.choose,if,parallel,repeat,wait_template,sequenceaction types.- All 5
RunModevariants (concurrency control via Tokio semaphore/mutex). - Integration test:
automations.yamlfrom ADR-134 migration fixture loads and runs correctly.
P4 — Blueprint system + ruflo agent condition (1 week)
- Blueprint YAML parser + input variable substitution.
- Optional ruflo agent condition:
condition: ruflo_agentwithquery: "..."routes to ruflo LLM (ADR-133 §3.3); gated by RUVIEW-POLICY. automation.reloadservice.- Performance benchmark: 100 automations loaded; 100 state changes/s; verify trigger evaluation stays < 5 ms per state change.
7. Risks
| Risk | Likelihood | Severity | Mitigation | Cross-ADR impact |
|---|---|---|---|---|
| MiniJinja gaps — some HA templates use Jinja2 features MiniJinja doesn't support (template inheritance, Python-specific filters) | Medium | Medium | Document the MiniJinja-vs-Jinja2 delta before P3 ships; provide a migration guide for affected templates; defer the 5% of templates that fail to a Python-compat shim (ADR-134) | ADR-134: migration tool must warn on templates that use unsupported Jinja2 features |
Template performance — synchronous MiniJinja in spawn_blocking adds overhead under high automation fan-out |
Low | Low | Benchmark at 50 automations each evaluating a template trigger on every state_changed (worst case); if > 2 ms add a template-evaluation cache keyed by (template_hash, relevant_entity_states) | ADR-127: state machine must expose a "relevant states snapshot" API for caching |
ADR-127 state machine API not frozen — trigger evaluators call hass.states.all() and subscribe to broadcasts; if those APIs change, trigger code must update |
High (early) | High | ADR-127 must freeze its public API before ADR-129 P2 begins; use a HomeCoreRef trait (version 1.0 stable) |
ADR-127 owns this dependency |
Complex action YAML — real-world automations use deeply nested choose/if/parallel blocks; parsing is non-trivial |
Medium | Medium | Use a corpus of 500 public HA automations from the HA community (MIT-licensed) as parse-test fixtures in CI | None |
8. Open questions
Q1: MiniJinja does not support all Python-specific Jinja2 filters (e.g. map, select, reject with Python lambda arguments). HA's homeassistant/helpers/template.py adds custom equivalents of several of these. How many real-world HA automations use these filters? A corpus analysis of public HA configs on GitHub would answer this before P3 implementation.
Q2: HA's template trigger supports a value_template that can reference trigger.to_state, trigger.from_state, and trigger.for. This requires passing trigger context into the template evaluation scope. Is this context threading straightforward in MiniJinja, or does it require a custom context type?
Q3: The conversation trigger in HA uses the Assist pipeline's intent matching to fire automations based on voice commands. HOMECORE-ASSIST (ADR-133) owns the pipeline. Should the conversation trigger be implemented in ADR-129 (automation engine dependency on ADR-133) or in ADR-133 (assist pipeline fires automation events that ADR-129 listens to)?
Q4: HA blueprints have a community sharing mechanism (blueprint.exchange). Should HOMECORE support importing blueprints from HA's blueprint exchange directly, or only local blueprints?
9. References
HA upstream
homeassistant/components/automation/__init__.py—AutomationEntity,AutomationConfig, trigger/condition/action pipelinehomeassistant/components/script/__init__.py—Script,ScriptEntity, run modes, action sequence executionhomeassistant/helpers/template.py—Templateclass,TemplateEnvironment, all HA-specific Jinja2 globals and filtershomeassistant/helpers/config_validation.py— voluptuous schema definitions for all automation/script YAML elementshomeassistant/components/automation/blueprint.py— Blueprint input substitution
This repo
docs/adr/ADR-127-homecore-state-machine-rust.md— state machine and event bus that triggers subscribe todocs/adr/ADR-133-homecore-assist-ruflo.md— ruflo agent condition + conversation trigger dependencydocs/adr/ADR-134-homecore-migration-from-python-ha.md— migration tool readsautomations.yaml
External
- minijinja crates.io — Jinja2-compatible template engine in Rust
- HA Automation Templating docs — HA-specific template globals reference