-
-
Notifications
You must be signed in to change notification settings - Fork 6
adr 003 cost scoring
Status: Accepted
Date: 2026-05-11
Deciders: Project maintainers
The HSEM planner needs to evaluate and compare candidate battery charge/discharge plans. The evaluation function must achieve two goals:
- Financial accuracy — produce an auditable cost figure that corresponds to real money (what the user will actually pay or save).
- Plan selection — provide a ranking metric that the candidate selector can use to pick the best plan, including trade-offs that have no direct monetary value (e.g., battery wear, safety constraints, future opportunity cost).
A naive single-aggregate approach (one number for everything) conflates these two purposes. A selector score that mixes real costs with synthetic penalties makes the "total cost" reported to the user un-auditable and misleading. Conversely, a pure monetary cost cannot express soft constraints like "don't drain the battery to zero just because the horizon is short."
We needed an architecture that cleanly separates financial reporting from plan selection without duplicating calculation logic.
We split the cost function into two distinct aggregates returned for every candidate plan:
total_cost = grid_import_cost − export_revenue + cycle_cost + conversion_loss_cost + tariff_cost
- Every term in
total_costcorresponds to a real monetary flow. - No synthetic penalties enter this aggregate.
- Suitable for auditing, bill comparison, and user-facing display.
- The value is comparable to the user's actual electricity bill for the horizon period.
score = total_cost + soc_penalties + grid_limit_penalty + override_penalty + terminal_soc_value
- Starts from
total_cost(always includes real money). - Adds synthetic penalties (quadratic SoC guard, grid power limit, override cost).
- Adds the terminal SoC opportunity cost — a value representing the lost future benefit of stored energy consumed during the horizon.
- The selector always picks the candidate with the lowest
score, not the lowesttotal_cost.
| Concern | total_cost |
score |
|---|---|---|
| Auditable money | ✅ Yes | ✅ (as subset) |
| Avoids drain-to-zero bias | ❌ No | ✅ (via terminal SoC value) |
| Avoids SoC bound violations | ❌ No | ✅ (via quadratic guard) |
| Picks cheapest plan | ✅ If penalties=0 | ✅ Always |
A single number cannot serve both purposes without one of them being wrong.
Terminal SoC value uses:
terminal_soc_value = (E_initial − E_final) × p_replacement
Where p_replacement = minimum future import price across the horizon.
Using the minimum price (not the average) prevents over-valuing stored energy during peak-price periods, which would bias the selector against discharging at the most profitable time.
penalty = weight * (soc - bound)**2 # if soc outside [min, max]Quadratic form heavily penalises large violations while tolerating tiny numerical rounding errors.
Slots marked time_passed are excluded from SoC penalty calculation because the SoC simulator writes estimated_battery_soc = 0.0 as a sentinel, which would generate a false penalty of weight * min_soc² per past slot — identical across all candidates but log-misleading.
-
total_costis auditable: every term maps to a real money flow. Users can compare it to their electricity bill. - The selector can express preferences that have no monetary value (e.g., "don't violate SoC bounds") without corrupting the financial aggregate.
- Clear separation of concerns: cost function returns two numbers; the selector uses one, diagnostics expose both.
- Adding a new penalty (e.g., carbon intensity) adds it to
scoreonly, leavingtotal_costuntouched.
- Callers must be aware of which aggregate to use. The wrong choice (using
total_costfor selection, orscorefor billing) produces incorrect results. - Slightly more complex API surface: every evaluation returns two floats instead of one.
- The terminal-SoC opportunity cost is a synthetic value — it is not money the user will actually pay or receive, but represents a lower-bound estimate of future import cost.
- Single weighted-sum approach was rejected because it conflates monetary and non-monetary terms, making the "total cost" neither auditable nor a pure selector score.
- Lexicographic ordering (first minimise cost, then minimise penalties) was rejected because it cannot express trade-offs between cost and safety (e.g., paying slightly more to avoid draining the battery).
-
Post-selection re-costing (compute pure cost after picking by score) was rejected because it introduces a possible mismatch between the selected plan and the reported cost, violating the
winner.cost == final_output.costinvariant.
For every planner run:
-
winner.score == final_output.score(no post-selection mutation) winner.total_cost == final_output.total_cost-
score >= total_costalways (penalties are non-negative, terminal value can be positive or negative) - When all penalties are zero and terminal-SoC value is zero:
score == total_cost
- 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