Skip to content

feat(planner): add EV SoC economics sensor comparing cost across target SoC and deadlines - #905

Open
woopstar wants to merge 2 commits into
mainfrom
feat/903-ev-soc-economics
Open

feat(planner): add EV SoC economics sensor comparing cost across target SoC and deadlines#905
woopstar wants to merge 2 commits into
mainfrom
feat/903-ev-soc-economics

Conversation

@woopstar

@woopstar woopstar commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an "EV SoC Economics" diagnostic sensor per EV that answers "what would it
cost to charge to X% by deadline Y?" — the real-money cost of charging to each
of 50/60/70/80/100% SoC by the next occurrence of 08:00 and by the next
occurrence of 17:00.

Each combination is computed by cloning the coordinator's last-used
PlannerInput, overriding only that EV's target-SoC/deadline fields
(ev_planned_load_target_soc_pct / ev_planned_load_deadline, or the
ev_second_* equivalents), and re-running the existing pure
run_planner(PlannerInput) -> PlannerOutput. PlannerOutput.plan_cost.total_cost
is read back per combination. No planner/MILP semantics change — this
calls the existing pure engine unmodified with different inputs.

No auto-recommended target is exposed — the raw cost/delta numbers are
surfaced and the user decides.

What changed

Planner (pure, no HA imports):

  • custom_components/hsem/planner/ev_soc_economics.pynext_time_of_day()
    helper, EVSoCEconomicsPoint/EVSoCEconomicsResult dataclasses, and
    compute_ev_soc_economics(). Guard clauses mirror
    ev_planner.build_ev_charging_plan()'s early-outs (not
    enabled/connected/smart-charging-disabled/zero capacity). Targets at or
    below the EV's current SoC cost 0.0 and skip solving entirely. Feasibility
    is a simple charger-power-based check independent of price.

Coordinator:

  • custom_components/hsem/coordinator_ev_soc_economics.py — new mixin, called
    from coordinator_cycle.py right after _run_planner_phase() returns.
    Throttled to at most every 30 minutes
    (EV_SOC_ECONOMICS_RECOMPUTE_MIN_SECONDS), requires
    self._last_planner_input to be set, solves both EVs in a single
    hass.async_add_executor_job() call, and re-checks the update-generation
    guard afterwards (same stale-cycle pattern as the main run_planner() call).
  • New fields on CoordinatorData / CoordinatorSharedState:
    ev_soc_economics, ev_second_soc_economics.

Sensors:

  • custom_sensors/ev_soc_economics_sensor.py (HSEMEVSoCEconomicsSensor) and
    custom_sensors/ev_second_soc_economics_sensor.py
    (HSEMEVSecondSoCEconomicsSensor) — mirror the existing
    ev_optimal_charging_plan sensors exactly (DIAGNOSTIC, RestoreEntity,
    ENUM device class). State is one of ready / not_connected /
    smart_charging_disabled / unavailable; the full cost/delta table lives
    in extra_state_attributes via EVSoCEconomicsResult.as_attributes().
  • New naming helpers in utils/sensornames/ev.py, registered in sensor.py
    under the existing hsem_ev_planned_load_enabled /
    hsem_ev_second_planned_load_enabled gates.
  • Translation keys added to translations/en.json and translations/da.json.

Dashboard:

  • Conditional markdown-table card per EV in
    custom_components/hsem/dashboards/dashboard_en.yaml's "EV Charging"
    section, rendering the points attribute grouped by deadline_label via
    Jinja's groupby filter (not custom:apexcharts-card — this is a small
    categorical table, not a time series). Mirrored into
    docs/dashboard-setup.md's "Full dashboard YAML" block; card-count table
    and "EV Charging tiles" reference table updated.

Docs:

  • New "EV SoC economics sensors" subsection in docs/sensors-reference.md
    under "EV charging plan sensors". docs/planner-spec.md is untouched — no
    planner semantics changed.

Tests

  • tests/planner/test_ev_soc_economics.pynext_time_of_day(), guard
    states (no run_planner() call), monotonic cost per deadline column,
    zero-cost/no-solve for already-met targets, price-independent feasibility,
    delta fields, second-EV field isolation (17 tests).
  • tests/test_coordinator.py — throttle window, first-call compute,
    no-op without _last_planner_input, single executor job for both EVs,
    stale-generation discard (6 tests).
  • tests/sensors/test_ev_soc_economics_sensor.py — state/attributes/
    availability/restore for both sensors (25 tests).
  • Fixed two pre-existing tests/test_ha_mock_integration.py bare-coordinator
    helpers/mocks that didn't know about the new coordinator fields/executor
    call.

Test and lint results

All four quality gates pass:

./scripts/quality.sh lint     # ruff format + ruff check + prettier — clean
./scripts/quality.sh typing   # mypy — 0 errors
./scripts/quality.sh quality  # pyright + vulture — 0 errors
./scripts/quality.sh test     # 3088 passed

Acceptance criteria (from #903)

  • compute_ev_soc_economics() returns a monotonically non-decreasing cost
    per target within each deadline column, for a fixed price/PV forecast.
  • Targets at or below the EV's current SoC cost 0.0 and do not trigger
    an extra run_planner() solve.
  • feasible is False when the charger's max power cannot physically
    reach the target by the deadline, independent of price.
  • Guard states (not_connected, smart_charging_disabled,
    unavailable) short-circuit without calling run_planner() at all.
  • Recomputation is throttled and stale-update-cycle safe.
  • The second-EV sensor only appears when
    hsem_ev_second_planned_load_enabled is set.
  • docs/sensors-reference.md documents both new sensors.
  • Bundled dashboard shows a conditional markdown-table card per EV,
    mirrored into docs/dashboard-setup.md.
  • Tests added covering the above.
  • ./scripts/quality.sh all passes.

Fixes #903

…et SoC and deadlines - Fixes #903

Adds a diagnostic sensor per EV that shows the real-money cost of charging
to each of 50/60/70/80/100% SoC by the next occurrence of 08:00 and 17:00,
by re-running the existing pure run_planner() on a cloned copy of the
coordinator's last-used PlannerInput with only that EV's target-SoC and
deadline fields overridden. No planner/MILP semantics change.

- New pure module planner/ev_soc_economics.py (next_time_of_day(),
  EVSoCEconomicsPoint/Result, compute_ev_soc_economics()).
- New coordinator mixin coordinator_ev_soc_economics.py, wired into
  coordinator_cycle.py right after _run_planner_phase(), throttled to at
  most every 30 minutes, both EVs solved in one executor job, with the
  same stale-update-cycle guard as the main planner run.
- Two new diagnostic sensors (ev_soc_economics_sensor.py,
  ev_second_soc_economics_sensor.py) mirroring the existing EV optimal
  charging plan sensors, gated on the existing EV enabled config flags.
- Conditional markdown-table dashboard cards per EV, mirrored into
  docs/dashboard-setup.md.
- Docs: new "EV SoC economics sensors" subsection in
  docs/sensors-reference.md.
- Tests: tests/planner/test_ev_soc_economics.py, coordinator
  throttle/gating/stale-cycle tests in tests/test_coordinator.py, and
  sensor tests in tests/sensors/test_ev_soc_economics_sensor.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(planner): add EV SoC economics sensor comparing cost across target SoC and deadlines

1 participant