feat(adr-118/p5.10): three HA operator blueprints (210/210 GREEN)
Iter 30. Ships the three ADR-122 §2.6 operator-ready Home Assistant
automation blueprints. Each blueprint binds to one BFLD MQTT entity
(presence / motion / identity_risk) and lets an HA operator import
+ configure without writing YAML by hand.
Added (under v2/crates/cog-ha-matter/blueprints/bfld/):
- presence-lighting.yaml
binary_sensor.<node>_bfld_presence ⇒ light.turn_on / turn_off
with a configurable hold_seconds delay before the off action
(ADR-122 §2.6 requirement: "configurable hold time")
- motion-hvac.yaml
sensor.<node>_bfld_motion ⇒ climate.set_temperature
Operator picks motion_threshold (default 0.3, per ADR §2.6),
delta_temperature_c (°C adjustment), and quiet_seconds debounce
- identity-risk-anomaly.yaml
sensor.<node>_bfld_identity_risk ⇒ notify.<target>
Two trigger paths:
- Absolute spike (raw score >= spike_threshold, default 0.8)
- Rolling 7-day z-score deviation (default 3 sigma)
Requires a Statistics helper entity for the baseline; documented
in the inline description and the blueprints README.
- README.md
Lists the three blueprints + privacy caveat for identity_risk
(only present at PrivacyClass::Anonymous; class 3 deployments
will fail validation by design)
Added (in v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs):
- 7 named tests using include_str! to embed each YAML at build time
and validate structure without adding a serde_yaml dep:
presence_lighting_blueprint_is_structurally_valid
motion_hvac_blueprint_is_structurally_valid
identity_risk_blueprint_is_structurally_valid
blueprints_carry_source_url_pointing_at_canonical_path
(catches path drift when files move)
presence_blueprint_uses_mqtt_integration_filter
motion_blueprint_uses_mqtt_integration_filter
identity_risk_blueprint_carries_privacy_class_caveat_in_description
(operators running class 3 should know not to install)
- Helper assert_required_blueprint_fields(yaml, name_substring, label)
enforces blueprint.{name,domain,input,trigger,action,mode} per HA spec
ACs progressed:
- ADR-122 §2.6 — all three blueprints shipped with the documented
configurable inputs (hold_seconds for #1, motion_threshold +
delta_temperature_c for #2, z_score_threshold + statistics_entity
for #3). Operator installs via HA UI; no YAML editing required.
- ADR-118 §1.5 privacy_mode visibility — identity-risk blueprint
documents the class-2-only availability so operators understand
why the blueprint fails on class-3 deployments.
Test config:
- cargo test --no-default-features → 72 passed
- cargo test → 210 passed (203 + 7)
Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker so iters 24 + 29
e2e tests actually run in CI with BFLD_MQTT_BROKER set.
- cog-ha-matter cargo crate-internal test that loads each blueprint
via serde_yaml + validates against an HA blueprint schema (instead
of the string-only checks here). Optional; current coverage is
sufficient to catch drift in the YAML files themselves.
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
74807a60c8
commit
820258e932
|
|
@ -0,0 +1,26 @@
|
|||
# BFLD HA Blueprints
|
||||
|
||||
Operator-ready Home Assistant automation blueprints for the BFLD entities
|
||||
published by `wifi-densepose-bfld`. Sourced from **ADR-122 §2.6**.
|
||||
|
||||
## Installing
|
||||
|
||||
Copy each `.yaml` file into your HA `blueprints/automation/` directory (or
|
||||
import via the HA UI: Settings → Automations & Scenes → Blueprints → Import).
|
||||
|
||||
## Available blueprints
|
||||
|
||||
| File | Purpose | BFLD entity consumed |
|
||||
|---|---|---|
|
||||
| `presence-lighting.yaml` | Turn a light on/off with BFLD occupancy | `binary_sensor.<node>_bfld_presence` |
|
||||
| `motion-hvac.yaml` | Adjust HVAC setpoint when motion crosses a threshold | `sensor.<node>_bfld_motion` |
|
||||
| `identity-risk-anomaly.yaml` | Notify operator on identity-risk z-score spike | `sensor.<node>_bfld_identity_risk` |
|
||||
|
||||
## Privacy notes
|
||||
|
||||
- `identity-risk-anomaly.yaml` requires `sensor.<node>_bfld_identity_risk` which is **only present at `privacy_class = Anonymous`** (per ADR-122 §2.1). At `privacy_class = Restricted` (e.g., care-home deployments) the entity is not advertised to HA at all, and this blueprint will fail validation — by design.
|
||||
- The `statistics_entity` input for `identity-risk-anomaly.yaml` requires the operator to first create an HA Statistics helper for the BFLD identity-risk sensor with a 7-day window. The blueprint reads `mean` + `standard_deviation` attributes.
|
||||
|
||||
## Source-of-truth blueprint structure tests
|
||||
|
||||
`v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs` validates each YAML at build time via `include_str!` and asserts the presence of the required HA-blueprint fields (`blueprint.name`, `blueprint.domain`, `input` block, `trigger`, `action`, `mode`).
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
blueprint:
|
||||
name: BFLD Identity-Risk Anomaly Notification
|
||||
description: >
|
||||
Notify the operator when BFLD's identity-risk score deviates significantly
|
||||
from its rolling 7-day baseline — a signal that the RF environment has
|
||||
shifted toward a higher-leakage regime (new AP firmware, attacker-grade
|
||||
sniffer in range, unusual propagation). Sourced from ADR-122 §2.6 and
|
||||
ADR-121 §2.4.
|
||||
domain: automation
|
||||
source_url: https://github.com/ruvnet/RuView/blob/main/v2/crates/cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml
|
||||
input:
|
||||
bfld_identity_risk:
|
||||
name: BFLD Identity Risk sensor
|
||||
description: The `sensor.<node>_bfld_identity_risk` entity (only present at privacy_class = Anonymous).
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
integration: mqtt
|
||||
notify_target:
|
||||
name: Notify target service
|
||||
description: HA notify service to call (e.g., notify.mobile_app_<phone>).
|
||||
selector:
|
||||
text: {}
|
||||
spike_threshold:
|
||||
name: Absolute spike threshold
|
||||
description: Trigger immediately when raw score >= this value.
|
||||
default: 0.8
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
max: 0.99
|
||||
step: 0.01
|
||||
z_score_threshold:
|
||||
name: Rolling z-score threshold
|
||||
description: Trigger when deviation from 7-day mean exceeds this many sigmas.
|
||||
default: 3.0
|
||||
selector:
|
||||
number:
|
||||
min: 1.5
|
||||
max: 6.0
|
||||
step: 0.5
|
||||
statistics_entity:
|
||||
name: Statistics helper entity for the 7-day baseline
|
||||
description: >
|
||||
An HA `statistics` integration entity computing mean + standard
|
||||
deviation of the BFLD identity-risk sensor over a 7-day window.
|
||||
Configure via Settings → Devices & Services → Helpers → Statistics.
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: !input bfld_identity_risk
|
||||
above: !input spike_threshold
|
||||
id: absolute_spike
|
||||
- platform: template
|
||||
value_template: >
|
||||
{% set raw = states(trigger.entity_id) | float(0) %}
|
||||
{% set mean = state_attr(!input statistics_entity, 'mean') | float(0) %}
|
||||
{% set sigma = state_attr(!input statistics_entity, 'standard_deviation') | float(0.01) %}
|
||||
{{ (raw - mean) / sigma >= z_score_threshold }}
|
||||
id: z_score_spike
|
||||
|
||||
variables:
|
||||
z_score_threshold: !input z_score_threshold
|
||||
|
||||
action:
|
||||
- service: !input notify_target
|
||||
data:
|
||||
title: BFLD Identity-Risk Anomaly
|
||||
message: >
|
||||
Node {{ trigger.entity_id }} identity-risk score is {{ states(trigger.entity_id) }}.
|
||||
Investigate possible RF-environment shift (new AP firmware, nearby sniffer,
|
||||
unusual multipath). See ADR-118 / ADR-121 for context.
|
||||
mode: single
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
blueprint:
|
||||
name: BFLD Motion-Aware HVAC
|
||||
description: >
|
||||
Adjust an HVAC climate entity's setpoint when BFLD's normalized motion
|
||||
score crosses a threshold, indicating active occupancy. Off-trigger
|
||||
restores the original setpoint after a debounce window. Sourced from
|
||||
ADR-122 §2.6.
|
||||
domain: automation
|
||||
source_url: https://github.com/ruvnet/RuView/blob/main/v2/crates/cog-ha-matter/blueprints/bfld/motion-hvac.yaml
|
||||
input:
|
||||
bfld_motion:
|
||||
name: BFLD Motion sensor
|
||||
description: The `sensor.<node>_bfld_motion` entity (0.0–1.0 scalar).
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
integration: mqtt
|
||||
target_climate:
|
||||
name: Climate entity to adjust
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
motion_threshold:
|
||||
name: Motion threshold
|
||||
description: Motion-score level above which HVAC is considered "active occupancy".
|
||||
default: 0.3
|
||||
selector:
|
||||
number:
|
||||
min: 0.05
|
||||
max: 0.95
|
||||
step: 0.05
|
||||
delta_temperature_c:
|
||||
name: Setpoint adjustment (°C)
|
||||
description: How much to raise the heating setpoint during active occupancy.
|
||||
default: 1.5
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
max: 5.0
|
||||
step: 0.5
|
||||
unit_of_measurement: "°C"
|
||||
quiet_seconds:
|
||||
name: Quiet hold (seconds)
|
||||
description: Continuous below-threshold time before restoring the original setpoint.
|
||||
default: 600
|
||||
selector:
|
||||
number:
|
||||
min: 60
|
||||
max: 7200
|
||||
unit_of_measurement: seconds
|
||||
|
||||
variables:
|
||||
motion_threshold: !input motion_threshold
|
||||
delta_c: !input delta_temperature_c
|
||||
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: !input bfld_motion
|
||||
above: !input motion_threshold
|
||||
id: occupied
|
||||
- platform: numeric_state
|
||||
entity_id: !input bfld_motion
|
||||
below: !input motion_threshold
|
||||
for:
|
||||
seconds: !input quiet_seconds
|
||||
id: quiet
|
||||
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: occupied
|
||||
sequence:
|
||||
- service: climate.set_temperature
|
||||
target: !input target_climate
|
||||
data_template:
|
||||
temperature: "{{ (state_attr(this.attributes.target.entity_id, 'temperature') | float(20.0)) + delta_c }}"
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: quiet
|
||||
sequence:
|
||||
- service: climate.set_temperature
|
||||
target: !input target_climate
|
||||
data_template:
|
||||
temperature: "{{ (state_attr(this.attributes.target.entity_id, 'temperature') | float(20.0)) - delta_c }}"
|
||||
mode: restart
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
blueprint:
|
||||
name: BFLD Presence-Driven Lighting
|
||||
description: >
|
||||
Turn a light on when BFLD reports occupancy on a chosen node, and off
|
||||
after a configurable hold period of continuous non-presence. Sourced
|
||||
from ADR-122 §2.6 of the wifi-densepose / RuView repository.
|
||||
domain: automation
|
||||
source_url: https://github.com/ruvnet/RuView/blob/main/v2/crates/cog-ha-matter/blueprints/bfld/presence-lighting.yaml
|
||||
input:
|
||||
bfld_presence:
|
||||
name: BFLD Presence sensor
|
||||
description: The `binary_sensor.<node>_bfld_presence` entity exposed by BFLD.
|
||||
selector:
|
||||
entity:
|
||||
domain: binary_sensor
|
||||
integration: mqtt
|
||||
target_light:
|
||||
name: Light to control
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
hold_seconds:
|
||||
name: Off-delay hold (seconds)
|
||||
description: How long the room must stay empty before the light turns off.
|
||||
default: 120
|
||||
selector:
|
||||
number:
|
||||
min: 5
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
mode: slider
|
||||
step: 5
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: !input bfld_presence
|
||||
to: "on"
|
||||
id: presence_on
|
||||
- platform: state
|
||||
entity_id: !input bfld_presence
|
||||
to: "off"
|
||||
for:
|
||||
seconds: !input hold_seconds
|
||||
id: presence_off
|
||||
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: presence_on
|
||||
sequence:
|
||||
- service: light.turn_on
|
||||
target: !input target_light
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: presence_off
|
||||
sequence:
|
||||
- service: light.turn_off
|
||||
target: !input target_light
|
||||
mode: restart
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
//! Validate the cog-ha-matter HA blueprints structurally — they're shipped
|
||||
//! YAML, so the test embeds each file at compile time via `include_str!` and
|
||||
//! string-checks the required HA-blueprint fields. Avoids adding a serde_yaml
|
||||
//! dep to BFLD for what is effectively a documentation-of-record asset.
|
||||
//!
|
||||
//! ADR-122 §2.6 specifies three blueprints; this test pins their structure.
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
const PRESENCE_LIGHTING: &str = include_str!(
|
||||
"../../cog-ha-matter/blueprints/bfld/presence-lighting.yaml"
|
||||
);
|
||||
const MOTION_HVAC: &str = include_str!(
|
||||
"../../cog-ha-matter/blueprints/bfld/motion-hvac.yaml"
|
||||
);
|
||||
const IDENTITY_RISK: &str = include_str!(
|
||||
"../../cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml"
|
||||
);
|
||||
|
||||
fn assert_required_blueprint_fields(yaml: &str, name_substring: &str, label: &str) {
|
||||
assert!(
|
||||
yaml.contains("blueprint:"),
|
||||
"{label}: missing top-level `blueprint:` key",
|
||||
);
|
||||
assert!(yaml.contains("name:"), "{label}: missing `name`");
|
||||
assert!(
|
||||
yaml.contains(name_substring),
|
||||
"{label}: name does not mention {name_substring}",
|
||||
);
|
||||
assert!(
|
||||
yaml.contains("domain: automation"),
|
||||
"{label}: missing `domain: automation`",
|
||||
);
|
||||
assert!(yaml.contains("input:"), "{label}: missing `input:` block");
|
||||
assert!(yaml.contains("trigger:"), "{label}: missing `trigger:`");
|
||||
assert!(yaml.contains("action:"), "{label}: missing `action:`");
|
||||
assert!(yaml.contains("mode:"), "{label}: missing `mode:`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_lighting_blueprint_is_structurally_valid() {
|
||||
assert_required_blueprint_fields(PRESENCE_LIGHTING, "Presence", "presence-lighting");
|
||||
assert!(PRESENCE_LIGHTING.contains("bfld_presence"));
|
||||
assert!(PRESENCE_LIGHTING.contains("light.turn_on"));
|
||||
assert!(PRESENCE_LIGHTING.contains("light.turn_off"));
|
||||
assert!(
|
||||
PRESENCE_LIGHTING.contains("hold_seconds"),
|
||||
"must expose configurable hold time per ADR-122 §2.6",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_hvac_blueprint_is_structurally_valid() {
|
||||
assert_required_blueprint_fields(MOTION_HVAC, "HVAC", "motion-hvac");
|
||||
assert!(MOTION_HVAC.contains("bfld_motion"));
|
||||
assert!(MOTION_HVAC.contains("climate.set_temperature"));
|
||||
assert!(
|
||||
MOTION_HVAC.contains("motion_threshold"),
|
||||
"must expose configurable threshold per ADR-122 §2.6",
|
||||
);
|
||||
assert!(
|
||||
MOTION_HVAC.contains("delta_temperature_c"),
|
||||
"must expose configurable ΔT per ADR-122 §2.6",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_risk_blueprint_is_structurally_valid() {
|
||||
assert_required_blueprint_fields(IDENTITY_RISK, "Identity-Risk", "identity-risk-anomaly");
|
||||
assert!(IDENTITY_RISK.contains("bfld_identity_risk"));
|
||||
assert!(
|
||||
IDENTITY_RISK.contains("z_score_threshold"),
|
||||
"must expose rolling z-score threshold per ADR-122 §2.6",
|
||||
);
|
||||
assert!(
|
||||
IDENTITY_RISK.contains("statistics_entity"),
|
||||
"must require an HA Statistics helper entity for the 7-day baseline",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprints_carry_source_url_pointing_at_canonical_path() {
|
||||
for (label, yaml, fname) in [
|
||||
("presence-lighting", PRESENCE_LIGHTING, "presence-lighting.yaml"),
|
||||
("motion-hvac", MOTION_HVAC, "motion-hvac.yaml"),
|
||||
("identity-risk-anomaly", IDENTITY_RISK, "identity-risk-anomaly.yaml"),
|
||||
] {
|
||||
let needle = format!(
|
||||
"source_url: https://github.com/ruvnet/RuView/blob/main/v2/crates/cog-ha-matter/blueprints/bfld/{fname}"
|
||||
);
|
||||
assert!(
|
||||
yaml.contains(&needle),
|
||||
"{label}: source_url drift — expected {needle}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_blueprint_uses_mqtt_integration_filter() {
|
||||
// The presence blueprint targets BFLD entities published via MQTT auto-
|
||||
// discovery; the entity selector must filter to integration: mqtt so
|
||||
// operators don't accidentally bind a non-BFLD presence sensor.
|
||||
assert!(PRESENCE_LIGHTING.contains("integration: mqtt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_blueprint_uses_mqtt_integration_filter() {
|
||||
assert!(MOTION_HVAC.contains("integration: mqtt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_risk_blueprint_carries_privacy_class_caveat_in_description() {
|
||||
// The description should hint at the class 2-only availability so operators
|
||||
// running Restricted (class 3) deployments don't waste time installing the
|
||||
// blueprint.
|
||||
assert!(
|
||||
IDENTITY_RISK.contains("privacy_class") || IDENTITY_RISK.contains("Anonymous"),
|
||||
"identity-risk blueprint description should reference privacy_class gating",
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue