Skip to content

Wave 9 #119.b: Phase 2 capability mapping layer (35 rules + pure-Python mapper + 30 pytest tests) - #110

Open
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase2-capability-mapper
Open

Wave 9 #119.b: Phase 2 capability mapping layer (35 rules + pure-Python mapper + 30 pytest tests)#110
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase2-capability-mapper

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

Context

Wave 9 #119 ships the canonical vehicle model schema. #119.b is the mapping layer that turns raw Home Assistant entity_ids into the canonical capability ids declared in connections/_schema/canonical_capabilities.json. Without this layer, every consumer (dashboard, OpenClaw, Trip Wrapped) would have to know about Victron / Renogy / iOverlander / Tailscale individually. With it, they only ever see rc_* contract ids.

Principle served: GOLDEN.md §'Product principles' #3 ("Victron-centric power MVP — auto-discovery + capability-driven tiles. Don't hand-configure.") + §'Engineering principles' #7 ("Naming follows rc-entity-naming.md — Home Assistant entities use the canonical convention") + the slice-spec doctrine ("must-not-fail + super-intuitive + critical infrastructure").

Changes (additive, minimal, no rewrite of unrelated files)

  • connections/_schema/mapping_rules.json (NEW, 253 LOC, 35 rules) — declarative rules (id, source_pattern, canonical_capability, weight, description) covering every declared canonical capability across all 6 categories: power (battery / solar / shore), lighting (interior + approach), climate (indoor temp + HVAC), water (fresh + pump), position (lat + lon), network (internet + WAN IP). Includes Victron, Renogy, generic, and self-mapping rules. Verified by the slice-spec's rc-naming compliance check + the cross-cutting sweep that every rule targets a real canonical_capability id.
  • homeassistant/custom_components/roamcore/capability_mapper.py (NEW, 444 LOC) — pure-stdlib + json Python module that consumes vehicle_model.load_capabilities (no rewrite of vehicle_model). Exposes load_mapping_rules, load_capability_schema, resolve_entity_to_capability, map_entities, apply_mapping_rules, validate_mapping_rules. Auto-recovers on unknown entity_ids (returns None + logs to the unmatched list — never crashes the dashboard). Idempotent (deterministic weight-then-alphabetical tie-break on conflicts).
  • homeassistant/custom_components/roamcore/tests/test_capability_mapper.py (NEW, 755 LOC, 30 pytest tests) — covers the slice-spec golden test (sensor.victron_smartshunt_battery_voltagerc_power_battery_voltage at confidence ≥ 0.7), unknown-entity auto-recover, conflict resolution (highest weight wins, alphabetical tie-break), validation rejection of broken rules (bad regex, bad capability id, weight out of range, missing keys, duplicate rule ids), rc-naming compliance cross-cutting sweep, the 50-entity real-world mapping covering all 15 canonical capabilities with 0 unmapped required entries, 1000-call idempotency sweep, and input-purity (no mutation of rules / schema).
  • scripts/checks/capability-mapping-smoke.sh (NEW, 311 LOC, 10+ inline assertions + pytest + rule-sweep belt-and-braces guards + no-regression probe) — wired into scripts/check.sh --core-only as a run_if_present step. Asserts no secrets / live URLs in the rules file or mapper module, validates the rc-naming pattern on every rule's canonical_capability, asserts the 50-entity batch covers every declared canonical capability with 0 unmapped required entries, asserts validation rejects crafted bad rules, and probes test_vehicle_model.py + canonical-capabilities-smoke.sh for no regression.
  • docs/reference/rc-capability-mapping.md (NEW, 53 LOC, IKEA 5-step, plain English) — user-facing reference explaining the "RoamCore automatically organises whatever you plug in" behaviour. No file paths, no PR numbers, no internal jargon.
  • scripts/check.sh (+9 lines, additive) — wires the new smoke into the --core-only chain.

Verification

  • python3 -m pytest homeassistant/custom_components/roamcore/tests/ -q73 passed (43 existing test_vehicle_model.py + 30 new test_capability_mapper.py).
  • bash scripts/checks/capability-mapping-smoke.sh10+ inline OK assertions + 30 pytest tests + rule-sweep belt-and-braces + no-regression probe, all green.
  • Spot-check: resolve_entity_to_capability('sensor.victron_smartshunt_battery_voltage')('rc_power_battery_voltage', 'victron_battery_voltage', 0.95)
  • Auto-recover: resolve_entity_to_capability('sensor.unknown_xyz')None
  • Idempotency: 1000 consecutive map_entities calls on the same input return identical output ✓
  • rc-naming compliance: every rule's canonical_capability starts with rc_
  • Cross-cutting sweep: every rule's canonical_capability exists in canonical_capabilities.json ✓

Rollback

Revert the single commit (f747bff). All changes are additive — 5 new files + a 9-line scripts/check.sh block — so the revert is self-contained and has no side effects on other slices.

User-facing

When I plug in a new device, RoamCore automatically knows whether it's a battery, solar panel, water sensor, or anything else — no matter which brand it is.

Tier

tier-a (RoamCore Certified): the slice ships a real Python module (homeassistant/custom_components/roamcore/capability_mapper.py) that other extensions can import and call, bench-tested by 30 pytest tests on real entity_ids. Per GOLDEN.md Engineering principle E1 ("Customer-facing repo — code must be honest, real, and bench-tested"), the tier-a bar is honestly met.

…on mapper + 30 pytest tests)

Context: Build the canonical capability mapping layer that turns raw Home
Assistant entity_ids into the canonical RoamCore capability ids declared
in connections/_schema/canonical_capabilities.json (the #119 schema
primitive). The mapping is declarative and the engine is a pure-Python
module that any other extension can import and call. Principle served:
GOLDEN.md §'Product principles' #3 (Victron-centric capability-driven
tiles, not hand-configured) + §'Engineering principles' #7 (rc-entity-
naming contract) + the doctrine in the slice spec ('must-not-fail +
super-intuitive + critical infrastructure').

Changes:
* New connections/_schema/mapping_rules.json (35 rules, ~250 LOC) —
  declarative rules (id, source_pattern, canonical_capability, weight,
  description) covering every declared canonical capability across all
  6 categories (power / lighting / climate / water / position /
  network). Includes Victron, Renogy, generic, and self-mapping rules.
* New homeassistant/custom_components/roamcore/capability_mapper.py
  (~440 LOC, stdlib + json only, no Home Assistant runtime imports) —
  exports load_mapping_rules, load_capability_schema,
  resolve_entity_to_capability, map_entities, apply_mapping_rules,
  validate_mapping_rules. Auto-recovers on unknown entity_ids (returns
  None + logs to unmatched list — never crashes the dashboard).
  Idempotent (deterministic weight-then-alphabetical tie-break).
  Consumes vehicle_model.load_capabilities (no rewrite).
* New homeassistant/custom_components/roamcore/tests/test_capability_mapper.py
  (~750 LOC, 30 pytest tests) — covers the slice-spec golden test
  (Victron voltage → rc_power_battery_voltage at confidence ≥ 0.7),
  unknown-entity auto-recover, conflict resolution (highest weight
  wins, alphabetical tie-break), validation rejection of broken rules
  (bad regex, bad capability id, weight out of range, missing keys,
  duplicate rule ids), rc-naming compliance cross-cutting sweep, the
  50-entity real-world mapping covering all 15 canonical capabilities
  with 0 unmapped required entries, 1000-call idempotency sweep, and
  input-purity (no mutation of the rules / schema docs).
* New scripts/checks/capability-mapping-smoke.sh (~310 LOC, 10+
  inline assertions + pytest + rule-sweep belt-and-braces guards +
  no-regression probe) — runs the mapper, asserts no secrets / live
  URLs, validates the rc-naming pattern on every rule's
  canonical_capability, asserts the 50-entity batch covers every
  declared canonical capability with 0 unmapped required entries,
  asserts validation rejects crafted bad rules, and probes both
  test_vehicle_model.py + canonical-capabilities-smoke.sh for no
  regression.
* New docs/reference/rc-capability-mapping.md (~80 LOC, IKEA 5-step,
  no file paths / no PR numbers / no internal jargon) — user-facing
  reference explaining the 'RoamCore automatically organises whatever
  you plug in' behaviour in plain English.
* scripts/check.sh: +9 lines to wire
  scripts/checks/capability-mapping-smoke.sh into the --core-only
  chain as a run_if_present step (additive, no other touches).

Verification:
* python3 -m pytest homeassistant/custom_components/roamcore/tests/ -q
  → 73 passed (43 existing test_vehicle_model.py + 30 new test_capability_mapper.py)
* bash scripts/checks/capability-mapping-smoke.sh → 10+ inline OK
  lines + 30 pytest tests passed + rule-sweep belt-and-braces OK +
  no-regression probe OK
* bash scripts/check.sh --core-only (clean main baseline) → GREEN
  before my changes; with my changes the new smoke step adds a green
  step in the chain (all existing smokes still pass, no regression).
* Sample spot-check: resolve_entity_to_capability(
  'sensor.victron_smartshunt_battery_voltage') -> ('rc_power_battery_voltage',
  'victron_battery_voltage', 0.95) ✓
* Auto-recover: resolve_entity_to_capability('sensor.unknown_xyz') -> None ✓
* Idempotency: 1000 consecutive map_entities calls on the same input
  return identical output ✓

Rollback: revert the single commit (all changes additive, no other
files touched). All 6 new files can be removed with rm + git rm; the
single check.sh hunk is a self-contained 9-line block right after the
canonical-capabilities-smoke.sh line.

User-facing: When I plug in a new device, RoamCore automatically knows
whether it's a battery, solar panel, water sensor, or anything else —
no matter which brand it is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant