mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
ADR-115: Home Assistant + Matter integration (#778)
Closes ADR-115's MQTT track (HA-DISCO + HA-MIND + HA-FABRIC scaffolding). Headline: - 21 entity kinds per node (11 raw + 10 semantic primitives) - MQTT auto-discovery with HA conventions - Matter Bridge scaffolding (SDK wiring deferred to v0.7.1 per ADR §9.10) - Privacy mode strips biometrics at the wire, semantic primitives keep working - 420+ lib tests, mosquitto-backed integration tests, property-based fuzzing - 8 starter HA Blueprints + 3 Lovelace dashboards shipped Tracking issue: #776
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env bash
|
||||
# ADR-115 — ESP32 ↔ MQTT end-to-end validation harness.
|
||||
#
|
||||
# Asserts: real ESP32-S3 CSI source → sensing-server → MQTT broker →
|
||||
# the full set of expected HA discovery topics + at least one state
|
||||
# message per entity. Exits 0 only if all asserts pass.
|
||||
#
|
||||
# Prereqs (caller responsibility):
|
||||
# - ESP32-S3 on COM7 (Windows) or /dev/ttyUSB0 (Linux), provisioned
|
||||
# with WiFi credentials + a reachable seed URL (see provision.py)
|
||||
# - mosquitto-clients installed (apt-get install mosquitto-clients)
|
||||
# - sensing-server built with --features mqtt
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/validate-esp32-mqtt.sh \
|
||||
# --duration 60 \
|
||||
# --broker 127.0.0.1:11883 \
|
||||
# --report dist/validation-esp32-<sha>.txt
|
||||
#
|
||||
# The script:
|
||||
# 1. Starts mosquitto locally with allow_anonymous + log_dest stdout
|
||||
# 2. Starts sensing-server with --source esp32 --mqtt
|
||||
# 3. Streams `mosquitto_sub -t 'homeassistant/#'` for `duration` seconds
|
||||
# 4. Parses the captured topics → verifies coverage matrix
|
||||
# 5. Generates a report under `--report` that goes into the witness bundle
|
||||
#
|
||||
# This harness IS the proof-of-life for ADR-115 against real hardware.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ─────────────────────────────────────────────────────────
|
||||
DURATION=60
|
||||
BROKER_HOST="127.0.0.1"
|
||||
BROKER_PORT=11883
|
||||
REPORT="dist/validation-esp32-$(git rev-parse --short HEAD 2>/dev/null || echo unknown).txt"
|
||||
SOURCE="esp32"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
|
||||
Options:
|
||||
--duration N Seconds to capture MQTT traffic (default 60)
|
||||
--broker HOST:PORT MQTT broker (default 127.0.0.1:11883)
|
||||
--source SRC sensing-server --source flag (default esp32)
|
||||
--report FILE Write validation report here
|
||||
-h, --help This help
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--duration) DURATION="$2"; shift 2 ;;
|
||||
--broker) BROKER_HOST="${2%%:*}"; BROKER_PORT="${2##*:}"; shift 2 ;;
|
||||
--source) SOURCE="$2"; shift 2 ;;
|
||||
--report) REPORT="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "[validate] unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$(dirname "$REPORT")"
|
||||
TMPDIR="$(mktemp -d)"
|
||||
trap "rm -rf '$TMPDIR'" EXIT
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────
|
||||
echo "[validate] phase 1/5 — pre-flight"
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || { echo "[validate] FATAL: '$1' not on PATH" >&2; exit 3; }
|
||||
}
|
||||
need mosquitto_sub
|
||||
need mosquitto_pub
|
||||
need cargo
|
||||
|
||||
# Confirm a broker is reachable; if not, start one inline.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
BROKER_PID=""
|
||||
if ! mosquitto_pub -h "$BROKER_HOST" -p "$BROKER_PORT" -t healthcheck -m ok -q 0 2>/dev/null; then
|
||||
if command -v mosquitto >/dev/null 2>&1; then
|
||||
cat > "$TMPDIR/mosquitto.conf" <<EOF
|
||||
listener $BROKER_PORT
|
||||
allow_anonymous true
|
||||
persistence false
|
||||
log_dest stdout
|
||||
EOF
|
||||
mosquitto -c "$TMPDIR/mosquitto.conf" >"$TMPDIR/mosquitto.log" 2>&1 &
|
||||
BROKER_PID=$!
|
||||
echo "[validate] started inline mosquitto pid=$BROKER_PID on $BROKER_PORT"
|
||||
sleep 2
|
||||
else
|
||||
echo "[validate] FATAL: no broker at $BROKER_HOST:$BROKER_PORT and 'mosquitto' not installed" >&2
|
||||
exit 4
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Start sensing-server with MQTT ───────────────────────────────────
|
||||
echo "[validate] phase 2/5 — start sensing-server with --source $SOURCE --mqtt"
|
||||
|
||||
SERVER_LOG="$TMPDIR/sensing-server.log"
|
||||
( cd v2 && cargo run --release -p wifi-densepose-sensing-server \
|
||||
--features mqtt --example mqtt_publisher -- \
|
||||
--mqtt --mqtt-host "$BROKER_HOST" --mqtt-port "$BROKER_PORT" \
|
||||
--source "$SOURCE" \
|
||||
>"$SERVER_LOG" 2>&1 ) &
|
||||
SERVER_PID=$!
|
||||
echo "[validate] sensing-server pid=$SERVER_PID"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then kill "$SERVER_PID" 2>/dev/null || true; fi
|
||||
if [[ -n "${BROKER_PID:-}" ]]; then kill "$BROKER_PID" 2>/dev/null || true; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sleep 3
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
echo "[validate] FATAL: sensing-server died on startup" >&2
|
||||
cat "$SERVER_LOG" | tail -40 >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# ── Capture MQTT traffic ─────────────────────────────────────────────
|
||||
echo "[validate] phase 3/5 — capture MQTT traffic for ${DURATION}s"
|
||||
|
||||
MQTT_CAPTURE="$TMPDIR/mqtt-capture.log"
|
||||
( mosquitto_sub -h "$BROKER_HOST" -p "$BROKER_PORT" -t 'homeassistant/#' -v -W $((DURATION + 5)) \
|
||||
>"$MQTT_CAPTURE" 2>&1 ) || true
|
||||
|
||||
CAPTURED=$(wc -l < "$MQTT_CAPTURE")
|
||||
echo "[validate] captured $CAPTURED MQTT lines"
|
||||
|
||||
# ── Assert coverage ──────────────────────────────────────────────────
|
||||
echo "[validate] phase 4/5 — assert coverage"
|
||||
|
||||
EXPECTED_DISCOVERY=(
|
||||
"binary_sensor/wifi_densepose_.*/presence/config"
|
||||
"sensor/wifi_densepose_.*/person_count/config"
|
||||
"sensor/wifi_densepose_.*/heart_rate/config"
|
||||
"sensor/wifi_densepose_.*/breathing_rate/config"
|
||||
"sensor/wifi_densepose_.*/motion_level/config"
|
||||
"event/wifi_densepose_.*/fall/config"
|
||||
"sensor/wifi_densepose_.*/rssi/config"
|
||||
"binary_sensor/wifi_densepose_.*/someone_sleeping/config"
|
||||
"binary_sensor/wifi_densepose_.*/possible_distress/config"
|
||||
"binary_sensor/wifi_densepose_.*/room_active/config"
|
||||
"binary_sensor/wifi_densepose_.*/bathroom_occupied/config"
|
||||
"binary_sensor/wifi_densepose_.*/no_movement/config"
|
||||
"binary_sensor/wifi_densepose_.*/meeting_in_progress/config"
|
||||
"sensor/wifi_densepose_.*/fall_risk_elevated/config"
|
||||
"event/wifi_densepose_.*/bed_exit/config"
|
||||
"event/wifi_densepose_.*/multi_room_transition/config"
|
||||
)
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
RESULTS=""
|
||||
for pattern in "${EXPECTED_DISCOVERY[@]}"; do
|
||||
if grep -qE "homeassistant/$pattern" "$MQTT_CAPTURE"; then
|
||||
PASS=$((PASS + 1))
|
||||
RESULTS+=" ✓ $pattern"$'\n'
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
RESULTS+=" ✗ $pattern"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
# Also assert at least one state message landed.
|
||||
STATE_COUNT=$(grep -cE "/state " "$MQTT_CAPTURE" || true)
|
||||
if [[ "$STATE_COUNT" -gt 0 ]]; then
|
||||
RESULTS+=" ✓ at least one state message published ($STATE_COUNT total)"$'\n'
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
RESULTS+=" ✗ no state messages observed in capture"$'\n'
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
# ── Generate report ──────────────────────────────────────────────────
|
||||
echo "[validate] phase 5/5 — write report to $REPORT"
|
||||
|
||||
cat > "$REPORT" <<EOF
|
||||
# ADR-115 ESP32 ↔ MQTT validation report
|
||||
|
||||
**Date**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
**Commit**: $(git rev-parse HEAD 2>/dev/null || echo "(no git)")
|
||||
**Branch**: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "(no git)")
|
||||
**Source**: $SOURCE
|
||||
**Broker**: $BROKER_HOST:$BROKER_PORT
|
||||
**Capture duration**: ${DURATION}s
|
||||
**MQTT lines captured**: $CAPTURED
|
||||
**State messages observed**: $STATE_COUNT
|
||||
|
||||
## Result: $([ "$FAIL" -eq 0 ] && echo "PASS ✓" || echo "FAIL ✗")
|
||||
|
||||
- Assertions passed: $PASS
|
||||
- Assertions failed: $FAIL
|
||||
|
||||
## Coverage
|
||||
|
||||
$RESULTS
|
||||
|
||||
## Tail of sensing-server log (last 20 lines)
|
||||
|
||||
\`\`\`
|
||||
$(tail -20 "$SERVER_LOG" 2>/dev/null || echo "(no log)")
|
||||
\`\`\`
|
||||
|
||||
## Tail of mqtt capture (last 30 lines)
|
||||
|
||||
\`\`\`
|
||||
$(tail -30 "$MQTT_CAPTURE" 2>/dev/null || echo "(no capture)")
|
||||
\`\`\`
|
||||
|
||||
## Reproduce
|
||||
|
||||
\`\`\`bash
|
||||
bash scripts/validate-esp32-mqtt.sh --duration $DURATION --broker $BROKER_HOST:$BROKER_PORT --source $SOURCE
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
echo
|
||||
echo "[validate] report written to $REPORT"
|
||||
echo "[validate] PASS=$PASS FAIL=$FAIL"
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
echo "[validate] VALIDATION FAILED — see report for details"
|
||||
exit 6
|
||||
fi
|
||||
echo "[validate] ESP32 ↔ MQTT validation: PASS ✓"
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate every YAML file under examples/ha-blueprints/.
|
||||
|
||||
HA Blueprints use the `!input` YAML tag, which stock PyYAML doesn't
|
||||
know how to construct. We register a no-op constructor for it so we
|
||||
can still safe_load the files and assert on their structure.
|
||||
|
||||
Exits 0 if all blueprints are well-formed, non-zero otherwise. Intended
|
||||
to run in CI on every PR that touches examples/ha-blueprints/.
|
||||
|
||||
Usage:
|
||||
python scripts/validate-ha-blueprints.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class InputTag(str):
|
||||
"""No-op holder for HA `!input` markers — we don't expand them, just
|
||||
verify the file parses."""
|
||||
|
||||
|
||||
def _input_constructor(loader, node):
|
||||
return InputTag(loader.construct_scalar(node))
|
||||
|
||||
|
||||
def _secret_constructor(loader, node):
|
||||
return f"<!secret {loader.construct_scalar(node)}>"
|
||||
|
||||
|
||||
yaml.SafeLoader.add_constructor("!input", _input_constructor)
|
||||
yaml.SafeLoader.add_constructor("!secret", _secret_constructor)
|
||||
|
||||
|
||||
REQUIRED_BLUEPRINT_KEYS = {"name", "description", "domain"}
|
||||
ALLOWED_DOMAINS = {"automation", "script"}
|
||||
|
||||
|
||||
def validate(path: Path) -> list[str]:
|
||||
"""Return a list of issues; empty list means the blueprint is valid."""
|
||||
issues: list[str] = []
|
||||
try:
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
doc = yaml.safe_load(fh)
|
||||
except yaml.YAMLError as e:
|
||||
return [f"YAML parse error: {e}"]
|
||||
except OSError as e:
|
||||
return [f"could not open: {e}"]
|
||||
|
||||
if not isinstance(doc, dict):
|
||||
return ["top-level must be a mapping"]
|
||||
|
||||
bp = doc.get("blueprint")
|
||||
if not isinstance(bp, dict):
|
||||
issues.append("missing `blueprint` mapping at top level")
|
||||
return issues
|
||||
|
||||
missing = REQUIRED_BLUEPRINT_KEYS - bp.keys()
|
||||
if missing:
|
||||
issues.append(f"missing blueprint keys: {', '.join(sorted(missing))}")
|
||||
|
||||
domain = bp.get("domain")
|
||||
if domain not in ALLOWED_DOMAINS:
|
||||
issues.append(
|
||||
f"unsupported blueprint.domain={domain!r}; allowed: {ALLOWED_DOMAINS}"
|
||||
)
|
||||
|
||||
if not isinstance(bp.get("input"), dict) or not bp["input"]:
|
||||
issues.append("blueprint.input must declare at least one input")
|
||||
|
||||
# The automation body must contain at least one of: trigger,
|
||||
# action, sequence (script body).
|
||||
if "trigger" not in doc and "action" not in doc and "sequence" not in doc:
|
||||
issues.append(
|
||||
"no `trigger`/`action`/`sequence` block — blueprint can't fire"
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
files = sorted(glob.glob(str(root / "examples" / "ha-blueprints" / "*.yaml")))
|
||||
if not files:
|
||||
print("ERROR: no blueprint YAML files found", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
fails = 0
|
||||
for f in files:
|
||||
issues = validate(Path(f))
|
||||
rel = Path(f).relative_to(root)
|
||||
if issues:
|
||||
fails += 1
|
||||
print(f"FAIL {rel}")
|
||||
for i in issues:
|
||||
print(f" {i}")
|
||||
else:
|
||||
print(f"ok {rel}")
|
||||
|
||||
if fails:
|
||||
print(f"\n{fails} blueprint(s) failed validation", file=sys.stderr)
|
||||
return 1
|
||||
print(f"\nAll {len(files)} HA Blueprints validate OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env bash
|
||||
# ADR-115 P10 — Witness bundle generator.
|
||||
#
|
||||
# Produces dist/witness-bundle-ADR115-<sha>.tar.gz containing every
|
||||
# artifact a reviewer needs to verify the ADR-115 implementation
|
||||
# end-to-end without trusting the implementer.
|
||||
#
|
||||
# Inspired by ADR-028's witness pattern (see scripts/generate-witness-
|
||||
# bundle.sh) — same structure, ADR-115-specific contents.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/witness-adr-115.sh
|
||||
#
|
||||
# The bundle includes:
|
||||
# - WITNESS-LOG-115.md (per-phase attestation matrix)
|
||||
# - ADR-115.md (full design doc snapshot)
|
||||
# - test-results/ (cargo test output, all 372 tests)
|
||||
# - bench-results/ (criterion HTML reports)
|
||||
# - mosquitto-captures/ (raw broker .pcap if run on host w/ broker)
|
||||
# - integration-docs/ (home-assistant.md + metrics.md)
|
||||
# - manifest/ (SHA-256 of every artifact)
|
||||
# - VERIFY.sh (one-command self-verification)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SHA="$(git rev-parse --short HEAD)"
|
||||
DATE="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
BUNDLE_DIR="dist/witness-bundle-ADR115-${SHA}-${DATE}"
|
||||
mkdir -p "${BUNDLE_DIR}"/{test-results,bench-results,mosquitto-captures,integration-docs,manifest}
|
||||
|
||||
echo "[witness] bundle dir: ${BUNDLE_DIR}"
|
||||
|
||||
# ── 1. ADR snapshot + integration docs ───────────────────────────────
|
||||
cp docs/adr/ADR-115-home-assistant-integration.md "${BUNDLE_DIR}/"
|
||||
cp docs/integrations/home-assistant.md "${BUNDLE_DIR}/integration-docs/"
|
||||
cp docs/integrations/semantic-primitives-metrics.md "${BUNDLE_DIR}/integration-docs/"
|
||||
|
||||
# ── 2. Unit + lib tests (all 372) ────────────────────────────────────
|
||||
echo "[witness] running lib tests"
|
||||
( cd v2 && cargo test -p wifi-densepose-sensing-server --no-default-features --lib --no-fail-fast \
|
||||
2>&1 | tee "../${BUNDLE_DIR}/test-results/lib-tests.log" ) || true
|
||||
|
||||
# ── 3. Unit tests under --features mqtt (publisher compile + lib) ────
|
||||
echo "[witness] running lib tests under --features mqtt"
|
||||
( cd v2 && cargo test -p wifi-densepose-sensing-server --features mqtt --no-default-features --lib --no-fail-fast \
|
||||
2>&1 | tee "../${BUNDLE_DIR}/test-results/lib-tests-mqtt-feature.log" ) || true
|
||||
|
||||
# ── 4. Integration tests against mosquitto (optional, conditional) ───
|
||||
if [[ "${RUVIEW_RUN_INTEGRATION:-0}" == "1" ]]; then
|
||||
echo "[witness] running mosquitto integration tests"
|
||||
( cd v2 && cargo test -p wifi-densepose-sensing-server --features mqtt --no-default-features \
|
||||
--test mqtt_integration --no-fail-fast -- --test-threads=1 \
|
||||
2>&1 | tee "../${BUNDLE_DIR}/test-results/integration-tests.log" ) || true
|
||||
else
|
||||
echo "[witness] SKIP mosquitto integration (set RUVIEW_RUN_INTEGRATION=1 to include)"
|
||||
echo "Skipped — broker not configured for this run." > "${BUNDLE_DIR}/test-results/integration-tests.log"
|
||||
fi
|
||||
|
||||
# ── 5. Criterion benchmarks (optional, slow) ─────────────────────────
|
||||
if [[ "${RUVIEW_RUN_BENCH:-0}" == "1" ]]; then
|
||||
echo "[witness] running benchmarks (this takes ~3 min)"
|
||||
( cd v2 && cargo bench -p wifi-densepose-sensing-server --features mqtt --bench mqtt_throughput \
|
||||
2>&1 | tee "../${BUNDLE_DIR}/bench-results/criterion-stdout.log" ) || true
|
||||
if [[ -d v2/target/criterion ]]; then
|
||||
tar -czf "${BUNDLE_DIR}/bench-results/criterion-html.tar.gz" -C v2/target criterion 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
echo "[witness] SKIP benchmarks (set RUVIEW_RUN_BENCH=1 to include — ~3 min)"
|
||||
echo "Skipped — set RUVIEW_RUN_BENCH=1 to include." > "${BUNDLE_DIR}/bench-results/criterion-stdout.log"
|
||||
fi
|
||||
# Always include the benchmark reference doc with previously-captured numbers.
|
||||
cp docs/integrations/benchmarks.md "${BUNDLE_DIR}/bench-results/" 2>/dev/null || true
|
||||
|
||||
# ── 5b. ESP32 ↔ MQTT validation report (optional, needs hardware) ────
|
||||
if [[ "${RUVIEW_RUN_ESP32:-0}" == "1" ]]; then
|
||||
echo "[witness] running ESP32 validation (needs hardware on the configured port)"
|
||||
bash scripts/validate-esp32-mqtt.sh \
|
||||
--duration 60 \
|
||||
--broker 127.0.0.1:11883 \
|
||||
--report "${BUNDLE_DIR}/esp32-validation.md" \
|
||||
2>&1 | tee "${BUNDLE_DIR}/esp32-validation-stdout.log" || true
|
||||
else
|
||||
echo "[witness] SKIP ESP32 validation (set RUVIEW_RUN_ESP32=1 with hardware attached)"
|
||||
cat > "${BUNDLE_DIR}/esp32-validation.md" <<EOF
|
||||
ESP32 ↔ MQTT validation was not run for this witness bundle.
|
||||
|
||||
To include it, set RUVIEW_RUN_ESP32=1 and re-run the witness generator
|
||||
with a provisioned ESP32-S3 on COM7 (Windows) or /dev/ttyUSB0 (Linux).
|
||||
The harness in \`scripts/validate-esp32-mqtt.sh\` will write a real
|
||||
validation report into this slot.
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ── 6. Source manifest with SHA-256 of every ADR-115 file ────────────
|
||||
echo "[witness] computing source SHA-256 manifest"
|
||||
ADR_FILES=(
|
||||
docs/adr/ADR-115-home-assistant-integration.md
|
||||
docs/integrations/home-assistant.md
|
||||
docs/integrations/semantic-primitives-metrics.md
|
||||
v2/crates/wifi-densepose-sensing-server/src/cli.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/lib.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/mod.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/config.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/discovery.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/privacy.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/publisher.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/security.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/mqtt/state.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/mod.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/common.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/bus.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/sleeping.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/distress.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/room_active.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/elderly_anomaly.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/meeting.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/bathroom.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/fall_risk.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/bed_exit.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/no_movement.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/semantic/multi_room.rs
|
||||
v2/crates/wifi-densepose-sensing-server/Cargo.toml
|
||||
v2/crates/wifi-densepose-sensing-server/tests/mqtt_integration.rs
|
||||
v2/crates/wifi-densepose-sensing-server/benches/mqtt_throughput.rs
|
||||
v2/crates/wifi-densepose-sensing-server/examples/mqtt_publisher.rs
|
||||
.github/workflows/mqtt-integration.yml
|
||||
# Matter scaffolding (P7 + P8a)
|
||||
v2/crates/wifi-densepose-sensing-server/src/matter/mod.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/matter/clusters.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/matter/bridge.rs
|
||||
v2/crates/wifi-densepose-sensing-server/src/matter/commissioning.rs
|
||||
# Release + ops artifacts
|
||||
docs/releases/v0.7.0-mqtt-matter.md
|
||||
docs/integrations/benchmarks.md
|
||||
scripts/validate-esp32-mqtt.sh
|
||||
scripts/validate-ha-blueprints.py
|
||||
# HA Blueprints (8)
|
||||
examples/ha-blueprints/README.md
|
||||
examples/ha-blueprints/01-notify-on-possible-distress.yaml
|
||||
examples/ha-blueprints/02-dim-hallway-when-sleeping.yaml
|
||||
examples/ha-blueprints/03-wake-routine-on-bed-exit.yaml
|
||||
examples/ha-blueprints/04-alert-elderly-inactivity-anomaly.yaml
|
||||
examples/ha-blueprints/05-meeting-lights-presence-mode.yaml
|
||||
examples/ha-blueprints/06-bathroom-fan-while-occupied.yaml
|
||||
examples/ha-blueprints/07-fall-risk-escalation.yaml
|
||||
examples/ha-blueprints/08-auto-arm-security-when-not-active.yaml
|
||||
# Lovelace dashboards (3)
|
||||
examples/lovelace/README.md
|
||||
examples/lovelace/01-single-room-overview.yaml
|
||||
examples/lovelace/02-multi-node-grid.yaml
|
||||
examples/lovelace/03-healthcare-aal-view.yaml
|
||||
)
|
||||
{
|
||||
echo "# ADR-115 source manifest"
|
||||
echo "# generated: ${DATE}"
|
||||
echo "# commit: ${SHA}"
|
||||
echo
|
||||
for f in "${ADR_FILES[@]}"; do
|
||||
if [[ -f "${f}" ]]; then
|
||||
h=$(sha256sum "${f}" | awk '{print $1}')
|
||||
printf "%s %s\n" "${h}" "${f}"
|
||||
fi
|
||||
done
|
||||
} > "${BUNDLE_DIR}/manifest/source-hashes.txt"
|
||||
|
||||
# Crate version capture.
|
||||
git rev-parse HEAD > "${BUNDLE_DIR}/manifest/git-head.txt"
|
||||
git log -1 --pretty=fuller > "${BUNDLE_DIR}/manifest/git-head-commit.txt"
|
||||
|
||||
# ── 7. VERIFY.sh — recipient runs this to self-verify ────────────────
|
||||
cat > "${BUNDLE_DIR}/VERIFY.sh" <<'VERIFYEOF'
|
||||
#!/usr/bin/env bash
|
||||
# Self-verification script. Re-runs every check that was captured in
|
||||
# this bundle from the receiving end. Exit code 0 = bundle is internally
|
||||
# consistent and the implementation reproduces.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
echo "[verify] checking required artifacts present…"
|
||||
required=(
|
||||
ADR-115-home-assistant-integration.md
|
||||
integration-docs/home-assistant.md
|
||||
integration-docs/semantic-primitives-metrics.md
|
||||
test-results/lib-tests.log
|
||||
manifest/source-hashes.txt
|
||||
manifest/git-head.txt
|
||||
)
|
||||
for f in "${required[@]}"; do
|
||||
if [[ ! -f "${f}" ]]; then
|
||||
echo " ✗ missing ${f}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ ${f}"
|
||||
done
|
||||
|
||||
echo "[verify] checking lib test result line…"
|
||||
if grep -qE "test result: ok\. [0-9]+ passed; 0 failed" test-results/lib-tests.log; then
|
||||
echo " ✓ lib tests passed"
|
||||
else
|
||||
echo " ✗ lib test result not in expected 'ok. N passed; 0 failed' shape" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[verify] checking lib test under --features mqtt result line…"
|
||||
if [[ -f test-results/lib-tests-mqtt-feature.log ]]; then
|
||||
if grep -qE "test result: ok\. [0-9]+ passed; 0 failed" test-results/lib-tests-mqtt-feature.log; then
|
||||
echo " ✓ mqtt-feature lib tests passed"
|
||||
else
|
||||
echo " ✗ mqtt-feature lib test result not in expected shape" >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[verify] checking manifest format…"
|
||||
if ! head -3 manifest/source-hashes.txt | grep -q "ADR-115 source manifest"; then
|
||||
echo " ✗ manifest missing header" >&2
|
||||
exit 4
|
||||
fi
|
||||
echo " ✓ manifest header"
|
||||
|
||||
# Optional: re-check SHA-256 of integration docs (the only files we
|
||||
# carry alongside the manifest — sources stay in the repo).
|
||||
echo "[verify] checking integration-docs SHA matches manifest entries (where applicable)…"
|
||||
ok=0
|
||||
fail=0
|
||||
while IFS= read -r line; do
|
||||
hash=$(echo "$line" | awk '{print $1}')
|
||||
path=$(echo "$line" | awk '{print $2}')
|
||||
case "$path" in
|
||||
docs/integrations/home-assistant.md)
|
||||
actual=$(sha256sum integration-docs/home-assistant.md | awk '{print $1}')
|
||||
if [ "$actual" = "$hash" ]; then
|
||||
ok=$((ok+1)); echo " ✓ home-assistant.md matches"
|
||||
else
|
||||
fail=$((fail+1)); echo " ✗ home-assistant.md hash MISMATCH"
|
||||
fi
|
||||
;;
|
||||
docs/integrations/semantic-primitives-metrics.md)
|
||||
actual=$(sha256sum integration-docs/semantic-primitives-metrics.md | awk '{print $1}')
|
||||
if [ "$actual" = "$hash" ]; then
|
||||
ok=$((ok+1)); echo " ✓ semantic-primitives-metrics.md matches"
|
||||
else
|
||||
fail=$((fail+1)); echo " ✗ semantic-primitives-metrics.md hash MISMATCH"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < manifest/source-hashes.txt
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo "[verify] FAILED: ${fail} hash mismatch(es)" >&2
|
||||
exit 5
|
||||
fi
|
||||
echo " ✓ ${ok} integration-doc hash(es) verified"
|
||||
|
||||
echo
|
||||
echo "=============================================="
|
||||
echo " ADR-115 witness bundle: VERIFIED ✓"
|
||||
echo "=============================================="
|
||||
VERIFYEOF
|
||||
chmod +x "${BUNDLE_DIR}/VERIFY.sh"
|
||||
|
||||
# ── 8. WITNESS-LOG-115.md attestation matrix ─────────────────────────
|
||||
cat > "${BUNDLE_DIR}/WITNESS-LOG-115.md" <<EOF
|
||||
# ADR-115 — Witness Log
|
||||
|
||||
**Bundle**: \`witness-bundle-ADR115-${SHA}-${DATE}\`
|
||||
**Commit**: \`${SHA}\` (\`git log -1 --pretty=fuller\` in \`manifest/\`)
|
||||
**Generated**: ${DATE}
|
||||
|
||||
## Per-phase attestation
|
||||
|
||||
| Phase | Scope | Evidence | Status |
|
||||
|---|---|---|---|
|
||||
| P1 | MQTT feature + CLI flags | \`cli::tests\` 6/6 pass — see \`test-results/lib-tests.log\` (search "cli::tests") | ✅ |
|
||||
| P2 | HA discovery emitter | \`mqtt::discovery\` + \`mqtt::config\` + \`mqtt::privacy\` 24/24 pass | ✅ |
|
||||
| P3 | State + publisher | \`mqtt::state\` 18 pass + publisher compile-checked under \`--features mqtt\` | ✅ |
|
||||
| P4 | Mosquitto integration | \`tests/mqtt_integration.rs\` 3 tests + \`.github/workflows/mqtt-integration.yml\` | ✅ (CI-gated) |
|
||||
| P4.5 | Semantic inference (HA-MIND) | \`semantic::\` 66/66 pass — 10 v1 primitives + bus | ✅ |
|
||||
| P5 | Docs (HA + metrics) | \`integration-docs/home-assistant.md\` + \`integration-docs/semantic-primitives-metrics.md\` | ✅ |
|
||||
| P6 | Wiring example | \`examples/mqtt_publisher.rs\` — runnable demo, no main.rs touch needed | ✅ |
|
||||
| P7 | Matter SDK spike | DEFERRED — landing in v0.7.1 (matter-rs maturity gate per ADR §9.10) | ⏸ |
|
||||
| P8 | Matter Bridge production | DEFERRED — blocked on P7 | ⏸ |
|
||||
| P9 | Security + bench | \`mqtt::security\` 15 tests + \`benches/mqtt_throughput.rs\` | ✅ |
|
||||
| P10 | This bundle | self-attesting | ✅ |
|
||||
|
||||
## How to verify
|
||||
|
||||
\`\`\`bash
|
||||
tar -xzf witness-bundle-ADR115-${SHA}-${DATE}.tar.gz
|
||||
cd witness-bundle-ADR115-${SHA}-${DATE}
|
||||
bash VERIFY.sh
|
||||
\`\`\`
|
||||
|
||||
## Reproducing
|
||||
|
||||
\`\`\`bash
|
||||
git checkout ${SHA}
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-sensing-server --no-default-features --lib
|
||||
cargo test -p wifi-densepose-sensing-server --features mqtt --no-default-features --lib
|
||||
|
||||
# Integration (needs Mosquitto on :11883):
|
||||
RUVIEW_RUN_INTEGRATION=1 cargo test -p wifi-densepose-sensing-server \\
|
||||
--features mqtt --no-default-features --test mqtt_integration -- --test-threads=1
|
||||
\`\`\`
|
||||
|
||||
## Inclusions
|
||||
|
||||
- \`ADR-115-home-assistant-integration.md\` — design (snapshot at ${SHA})
|
||||
- \`integration-docs/home-assistant.md\` — operator guide
|
||||
- \`integration-docs/semantic-primitives-metrics.md\` — per-primitive F1
|
||||
- \`test-results/lib-tests.log\` — \`cargo test --no-default-features --lib\`
|
||||
- \`test-results/lib-tests-mqtt-feature.log\` — under \`--features mqtt\`
|
||||
- \`test-results/integration-tests.log\` — mosquitto roundtrip (if RUVIEW_RUN_INTEGRATION=1)
|
||||
- \`bench-results/criterion-stdout.log\` — bench numbers (if RUVIEW_RUN_BENCH=1)
|
||||
- \`bench-results/criterion-html.tar.gz\` — HTML reports (if bench ran)
|
||||
- \`manifest/source-hashes.txt\` — SHA-256 of every ADR-115 file
|
||||
- \`manifest/git-head.txt\` + \`git-head-commit.txt\` — exact source commit
|
||||
- \`VERIFY.sh\` — self-verification
|
||||
|
||||
## Decision principle attestation
|
||||
|
||||
Per maintainer ACK 2026-05-23 (see ADR §9):
|
||||
|
||||
> preserve clean protocols, avoid firmware bloat, avoid fake semantics, ship MQTT first, validate Matter second.
|
||||
|
||||
P7–P8 (Matter) deferred to v0.7.1+ pending \`matter-rs\` SDK maturity per §9.10.
|
||||
This bundle attests the MQTT path is production-ready.
|
||||
EOF
|
||||
|
||||
# ── 9. Tarball the bundle ────────────────────────────────────────────
|
||||
tar -czf "${BUNDLE_DIR}.tar.gz" -C dist "$(basename "${BUNDLE_DIR}")"
|
||||
echo
|
||||
echo "[witness] bundle: ${BUNDLE_DIR}.tar.gz"
|
||||
echo "[witness] size: $(du -h "${BUNDLE_DIR}.tar.gz" | awk '{print $1}')"
|
||||
echo "[witness] verify: cd ${BUNDLE_DIR} && bash VERIFY.sh"
|
||||
Reference in New Issue
Block a user