diff --git a/v2/crates/cog-ha-matter/blueprints/bfld/README.md b/v2/crates/cog-ha-matter/blueprints/bfld/README.md new file mode 100644 index 00000000..8e987852 --- /dev/null +++ b/v2/crates/cog-ha-matter/blueprints/bfld/README.md @@ -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._bfld_presence` | +| `motion-hvac.yaml` | Adjust HVAC setpoint when motion crosses a threshold | `sensor._bfld_motion` | +| `identity-risk-anomaly.yaml` | Notify operator on identity-risk z-score spike | `sensor._bfld_identity_risk` | + +## Privacy notes + +- `identity-risk-anomaly.yaml` requires `sensor._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`). diff --git a/v2/crates/cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml b/v2/crates/cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml new file mode 100644 index 00000000..298ce1e9 --- /dev/null +++ b/v2/crates/cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml @@ -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._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_). + 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 diff --git a/v2/crates/cog-ha-matter/blueprints/bfld/motion-hvac.yaml b/v2/crates/cog-ha-matter/blueprints/bfld/motion-hvac.yaml new file mode 100644 index 00000000..ca6c81f6 --- /dev/null +++ b/v2/crates/cog-ha-matter/blueprints/bfld/motion-hvac.yaml @@ -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._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 diff --git a/v2/crates/cog-ha-matter/blueprints/bfld/presence-lighting.yaml b/v2/crates/cog-ha-matter/blueprints/bfld/presence-lighting.yaml new file mode 100644 index 00000000..cc1b1778 --- /dev/null +++ b/v2/crates/cog-ha-matter/blueprints/bfld/presence-lighting.yaml @@ -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._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 diff --git a/v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs b/v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs new file mode 100644 index 00000000..bd7d5b31 --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs @@ -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", + ); +}