-
-
Notifications
You must be signed in to change notification settings - Fork 6
adr 001 planner extraction
Status: Accepted (retrospective)
Date: 2026-05-08
The HSEM integration originally had the planning logic tightly coupled with Home Assistant runtime concepts — sensor states, coordinator callbacks, and entity models were all mixed together in the same modules. This caused several problems:
-
Testing friction: Every test required a Home Assistant test harness
(
pytest-homeassistant-custom-component), making tests slow, fragile, and difficult to write. - Non-determinism: Runtime state leaks (e.g. HA event loops, sensor availability races) could produce different planner outputs for the same input data.
- Slow iteration: Running a single unit test required loading HA core, sensors, and the coordinator — a multi-second overhead per test.
- Poor separation of concerns: Business logic (charge/discharge scheduling, cost calculation, SoC simulation) was interleaved with infrastructure (config entry parsing, entity state reading, service calls).
The project needed a clear boundary between "what the planner computes" and "how HA delivers inputs and consumes outputs."
Extract all planning logic into a pure-Python layer with zero Home Assistant
imports. This layer lives in custom_components/hsem/planner/ and depends only
on the Python standard library (plus scipy for the optional MILP solver).
flowchart TD
A[HA integration layer\ncoordinator, sensors, services, flows]
B[Pure-Python planner engine\nplanner, models]
C[HA integration layer\ncoordinator to sensors to hardware writes]
A -->|PlannerInput dataclass\npure data, no HA objects| B
B -->|PlannerOutput dataclass\npure data, no HA objects| C
-
models/planner_inputs.pyandmodels/planner_outputs.pydefine the boundary dataclasses. These are pure Python — they contain only fields, no HA entity references, nohassobjects. -
planner/engine_core.pyorchestrates the full planning pipeline: slot population → scheduling → candidate generation → SoC simulation → cost scoring → selection. -
coordinator.py(HA-dependent) is responsible for:- Reading HA entity states via
state_collector.py - Building
PlannerInputfromSensorConfig+LiveState - Calling the planner engine
- Writing results back to HA sensors
- Reading HA entity states via
| Module | Responsibility | HA imports |
|---|---|---|
planner/engine_core.py |
Pipeline orchestration | None |
planner/slot_population.py |
Build time horizon | None |
planner/charge_scheduler.py |
Charge schedule logic | None |
planner/discharge_scheduler.py |
Discharge schedule logic | None |
planner/candidate_generator.py |
Candidate plan generation | None |
planner/candidate_selector.py |
Candidate scoring + selection | None |
planner/cost_function.py |
Cost/score calculation | None |
planner/soc_simulation.py |
Battery SoC forward sim | None |
planner/milp_optimizer.py |
LP solver (scipy HiGHS) | None |
planner/ev_planner.py |
EV charging plan builder | None |
planner/engine_explanation.py |
Human-readable explanations | None |
-
PlannerInputcontains only primitives, dataclasses, and enums — no HAEntity,ConfigEntry, orhassreferences. -
PlannerOutputcontainsPlannedSlot, candidate metadata, and cost breakdowns — all plain dataclasses. - The
coordinator_builder.pymodule bridges the gap: it converts HA types toPlannerInputvia pure mapping functions.
-
Deterministic planner: Same
PlannerInputalways produces the samePlannerOutput, enabling reproducible debugging and regression testing. -
Fast unit tests: Planner tests run in < 10 ms with plain
pytest— no HA harness needed. A full planning cycle completes in < 100 ms on commodity hardware. -
Clear testing strategy: Planner logic tests are pure
pytest. Integration tests (coordinator → planner → output) use the HA test harness but only need to verify the boundary, not the internal logic. - Separate evolution: Planner math can be improved without touching HA integration code, and vice versa.
- Open-source reuse: The pure-Python planner could potentially be reused by other Home Assistant integrations or standalone tools.
- Two sets of tests: Planner tests (pure pytest) + integration tests (HA test harness). Developers must understand both testing approaches.
-
Mapping overhead:
coordinator_builder.pymust faithfully map HA types toPlannerInput— any mismatch produces silent planner errors. -
Slight indirection: Adding a new planner input requires updating both
the HA side (state collector) and the model layer (
PlannerInput).
- The
coordinator_builder.pymapping functions are kept as thin, purely structural transformations — no business logic. - Integration tests in
tests/test_coordinator.pyverify that a full HA cycle produces a validPlannerOutput. -
PlannerInputandPlannerOutputhave strict type annotations caught bypyrightduring CI.
Rejected because: Testing friction and non-determinism made the planner difficult to maintain. Every change required full HA test harness setup.
Rejected because: Would have created leaky abstractions — planner methods would need to accept or return HA types at some boundary. Pure data boundary is simpler and more testable.
Rejected because: HSEM is a Home Assistant integration; extracting the planner as a separate pip package would require managing versioning, releases, and dependency sync. The current in-repo separation achieves the same decoupling without the overhead of a library release process.
custom_components/hsem/planner/custom_components/hsem/models/planner_inputs.pycustom_components/hsem/models/planner_outputs.pydocs/planner-spec.mddocs/architecture-overview.md
- Home — User-facing overview: features, FAQ, working modes, battery schedules, excess export, consumption sensors
- Battery Charging Economics — How to calculate the minimum charging price for a battery schedule
- Architecture Overview — System context, layered architecture, module map, planning pipeline
- Planner Specification — Normative — all planner invariants, rules, and constraints
- Planner Technical Guide — How the planner works with worked examples
- Cost Function Math — Complete mathematical formulation of the 8-term cost function
- Energy Accounting — Physical energy flow model, SoC simulation, efficiency math
- Candidate Generation — How candidates are generated, assumptions, partial-SoC
- MILP Optimization — Full LP formulation, variable layout, constraints, and solver pipeline
- Consumption Prediction — Weighted-average model, IQR outlier detection, spike suppression
- Safety Modes — Degraded mode, read-only gate, write-verify applier, runtime resolver
- Price Scaling — EDS price scaling, eds_share conversion factor
- Services Reference — All 4 HSEM services with examples
- Sensors Reference — Complete entity reference: all sensor, select, switch, number, and time entities
- Dashboard Setup — Step-by-step ApexCharts dashboard with full YAML, layout reference, and troubleshooting
- Config Flow Reference — Every config/options flow step and field
- EV Charge Plan Setup — EV planned load configuration guide
- EV Surplus Charging Automation — Wire your physical EV charger (go-e, Easee, Zaptec) to follow HSEM surplus recommendations
- EV Optimal Charging Template — Legacy Home Assistant template sensor for cost-optimal EV charging
- Forecast Accuracy Tracking — Forecast vs actual tracking system
- Huawei Entities — Canonical HA entity ID reference
- Troubleshooting Guide — Diagnose and fix common problems: missing data, wrong prices, write failures, battery behaviour
- Quality Checks — Static quality tools and CI configuration