Conversation
Design for replacing the scattered energy balance logic in the simulator engine with a modular, component-based energy system. Components (GridMeter, PVSource, BESSUnit, LoadGroup) resolve on a PanelBus with role-based ordering and conservation enforcement.
12-task phased plan: build energy system in isolation (Phase 1), wire into engine (Phase 2), eliminate old paths (Phase 3), and remove dead code (Phase 4).
Remove BSEE update()/integrate_energy() and related dead constants, fields, and imports now that EnergySystem handles all power-flow resolution and SOE bookkeeping. The engine syncs results back to BSEE each tick; BSEE retains only identity and grid-state properties.
The BatteryStorageEquipment class was a thin facade that held identity properties, schedule resolution, and grid state derived from forced_offline. All of these are now provided directly by BESSUnit (identity + schedule) and EnergySystem (grid_state, dominant_power_source, islandable). The circular state-sync from SystemState back to BSEE each tick is eliminated — the engine reads values straight from the energy system's resolved state.
Circuits without a recorder_entity are user-added and did not exist in the baseline system. The Before pass now returns 0 power for these circuits instead of letting the behavior engine synthesise values that leak into the Before graph.
Replace _aggregate_modeling_at_ts with two focused methods: - _collect_circuit_powers_at_ts: pure per-circuit power collection - _powers_to_energy_inputs: converts circuit powers to PowerInputs Both Before and After passes now follow the same pattern: collect circuit powers, feed into an EnergySystem, read derived values. The Before pass only includes recorder-backed circuits (baseline system). The After pass includes all current circuits. No if-guards needed — circuit participation is determined by set membership.
Remove bess_requested_w from PowerInputs — the energy system now sets discharge/charge power to the max inverter rate for the scheduled state. The GFE throttle and SOE bounds naturally limit actual power to what the home needs, matching how a Powerwall or similar residential BESS operates.
New modes: Self-Consumption (default, discharge to offset grid import, charge from PV excess only), Time-of-Use (user-set hourly schedule), Backup Only (hold at full SOC, discharge during outages). Removed solar-gen and solar-excess modes and all associated two-pass tick machinery. Self-consumption charges only from actual PV excess, never from grid.
The After label was hiding the breakdown when exported < 0.5 kWh, which happens when BESS absorbs all PV excess. Now both Before and After always show the full breakdown for clarity. Also hide discharge presets, active days, and hourly schedule when charge mode is Self-Consumption or Backup Only — those controls only apply to Time-of-Use mode.
Battery entities now show only installation-relevant fields: Name, Nameplate Capacity, Backup Reserve, Charge Power, Discharge Power. Hidden: Energy Profile (Typical/Min/Max Power), Priority, Relay Behavior — these are fixed for a real BESS installation.
The wait cursor was only applied to individual trigger elements, so users could still click elsewhere during start/stop/restart, clone, and modeling setup. Add a body.page-busy class that blocks all interaction page-wide for these slow paths.
There was a problem hiding this comment.
Pull request overview
This PR bumps to 1.0.7 and replaces the simulator’s scattered grid/PV/BESS power-flow logic with a new component-based EnergySystem used by snapshots, dashboard summary, and modeling, alongside UI updates for new battery charge modes and improved busy-state handling.
Changes:
- Introduce
span_panel_simulator.energy(bus/components/types/system) plus comprehensive 3-layer test suite for dispatch/islanding/SOE behavior. - Refactor
DynamicSimulationEngineto delegate power-flow resolution toEnergySystem(and remove legacy BSEE path). - Update dashboard UI/config defaults to new BESS charge modes (self-consumption/custom/backup-only) and add page-level busy overlay.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/span_panel_simulator/engine.py |
Integrates EnergySystem into snapshots, dashboard summary, and modeling passes; removes solar-excess two-pass logic. |
src/span_panel_simulator/energy/types.py |
Adds core dataclasses/enums for energy dispatch inputs/outputs and configs. |
src/span_panel_simulator/energy/components.py |
Implements Grid/PV/BESS/Load components including SOE integration and scheduling. |
src/span_panel_simulator/energy/bus.py |
Adds role-ordered bus resolution with conservation accounting. |
src/span_panel_simulator/energy/system.py |
Implements EnergySystem.from_config() and tick() dispatch including charge modes and islanding behavior. |
src/span_panel_simulator/energy/__init__.py |
Re-exports energy public API. |
src/span_panel_simulator/bsee.py |
Deleted legacy BSEE implementation. |
src/span_panel_simulator/behavior_mutable_state.py |
Removes solar-excess mutable state field. |
tests/test_energy/test_components.py |
Layer 1 unit tests for components and SOE integration. |
tests/test_energy/test_bus.py |
Layer 2 integration tests for bus conservation and dispatch interactions. |
tests/test_energy/test_scenarios.py |
Layer 3 scenario tests for islanding, charge modes, modeling deltas, and independence. |
tests/test_energy/__init__.py |
Test package init. |
src/span_panel_simulator/dashboard/defaults.py |
Updates default battery behavior to self-consumption and new inverter rates/schedules. |
src/span_panel_simulator/dashboard/config_store.py |
Updates charge-mode defaults and validation to new modes. |
src/span_panel_simulator/config_types.py |
Updates BatteryBehavior.charge_mode literal types. |
src/span_panel_simulator/dashboard/templates/partials/battery_profile_editor.html |
Updates charge mode options and hides schedule UI when not applicable. |
src/span_panel_simulator/dashboard/templates/partials/entity_edit.html |
Hides fields that don’t apply to battery entities. |
src/span_panel_simulator/dashboard/templates/partials/modeling_view.html |
Always displays imported/exported breakdown in labels. |
src/span_panel_simulator/dashboard/templates/partials/panels_list_rows.html |
Adds page-level busy state during clone. |
src/span_panel_simulator/dashboard/templates/base.html |
Adds global HTMX busy-state handling and extends busyFetch(). |
src/span_panel_simulator/dashboard/static/dashboard.css |
Adds body.page-busy interaction blocking + cursor styling. |
span_panel_simulator/CHANGELOG.md |
Adds 1.0.7 release notes. |
docs/superpowers/specs/2026-03-28-component-energy-system-design.md |
Adds design spec for the component energy system. |
docs/superpowers/plans/2026-03-28-component-energy-system.md |
Adds implementation plan for the refactor. |
Comments suppressed due to low confidence (1)
src/span_panel_simulator/engine.py:1396
- The
compute_modeling_data()docstring still references applying “BSEE” in the After pass, but this implementation now builds and ticksEnergySysteminstances instead. Please update the docstring to match the current behavior so future changes don’t rely on outdated assumptions.
async def compute_modeling_data(self, horizon_hours: int) -> dict[str, Any]:
"""Compute Before/After modeling data over recorder history.
Performs **read-only** passes — no runtime state is mutated.
**Before** uses HA recorder replay wherever ``recorder_entity`` data
exists (ignores ``user_modified``), with site power **without** BESS.
**After** uses current templates (SYN / overrides) and applies BSEE
for grid and battery traces.
PV curtailment: when islanded, hybrid inverters now reduce output to match load + achievable BESS charge, mirroring real MPPT setpoint behavior. Prevents unbalanced bus states during grid outages with high solar production. Bug fixes from code review: - SOE power limits now return watts (not watt-seconds) using conservative bounds over the max integration interval - islandable flag consulted in tick() for PV online decisions - Custom TOU mode resolves schedule internally instead of relying on externally pre-computed state - Hybrid inverter detection reads template priority field to match config_store persistence - Modeling docstring updated to reflect EnergySystem usage Tightened energy module encapsulation: PowerInputs no longer carries bess_scheduled_state — all scheduling is resolved inside EnergySystem.tick(). Engine only provides raw measurements.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/span_panel_simulator/engine.py:1518
- In
compute_modeling_data(),battery_power_arris derived fromEnergySystem.tick(), but per-circuit series (circuit_arrays_after) are still populated frompowers_a(behavior engine output). For battery circuits, the behavior engine returns near-idle power in self-consumption/backup-only, so overlays and per-circuit graphs can contradict the battery series. Consider overwriting the bidirectional (battery) circuit’s entry inpowers_a(and possiblypowers_b) with the energy system’s effective battery magnitude so circuit overlays stay consistent with the resolved system state.
# --- After pass: all current circuits ---
powers_a = self._collect_circuit_powers_at_ts(
ts,
cloned_behavior,
all_circuit_ids,
use_recorder_baseline=False,
)
inputs_a = self._powers_to_energy_inputs(powers_a)
state_a = after_energy_system.tick(ts, inputs_a)
grid_after = state_a.grid_power_w
batt_after = state_a.bess_power_w
if state_a.bess_state == "discharging":
batt_after = -batt_after
site_power_arr.append(round(grid_before, 1))
pv_before_arr.append(round(pv_before, 1))
grid_power_arr.append(round(grid_after, 1))
pv_after_arr.append(round(state_a.pv_power_w, 1))
battery_power_arr.append(round(batt_after, 1))
battery_before_arr.append(round(batt_before, 1))
for cid in self._circuits:
circuit_arrays_before[cid].append(round(powers_b.get(cid, 0.0), 1))
circuit_arrays_after[cid].append(round(powers_a.get(cid, 0.0), 1))
Clone dialog now checks for filename collisions before prompting, pre-filling a safe auto-suffixed name. If the user manually types an existing filename, an in-page modal with an explicit Overwrite button appears instead of silently replacing the file. Clone-from-panel also gains a confirmation step with overwrite/rename options. PV curtailment during islanding is now reflected back to producer circuit snapshots so dashboard power readings stay consistent with the resolved energy system state. Hybrid inverter type derivation fixed to use PV config rather than template priority.
Defines architecture for OpenEI URDB integration, simulator-wide rate caching, cost engine, and modeling view cost display.
Modeling view: replace inline kWh labels with a summary table above each chart showing Full Horizon and Visible Range rows. The After table includes a Difference column with color-coded delta from Before. Fix BESSUnit.integrate_energy() to loop in sub-steps of _MAX_INTEGRATION_DELTA_S instead of capping and discarding the remainder, so 3600s modeling steps integrate correctly. Update compute_modeling_data() docstring to reflect current EnergySystem usage.
10-task plan covering rate types, resolver, cost engine, cache, OpenEI client, API endpoints, engine wiring, and modeling view UI.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
src/span_panel_simulator/engine.py:1120
- During forced grid-offline shedding, all
bidirectionalcircuits are exempted as “battery” (continue). This will incorrectly exempt EVSE circuits (also bidirectional) from shedding and from being zeroed when the panel should be dead. Gate this exemption onbattery_behavior.enabled(or on_find_battery_circuit()/device_type) rather thanenergy_mode == "bidirectional".
# Battery: never shed
if circuit.energy_mode == "bidirectional":
continue
…uits Add _is_battery_circuit() to identify the configured BESS by checking battery_behavior.enabled. Both _collect_power_inputs() and _powers_to_energy_inputs() now use this instead of blanket energy_mode != "bidirectional", so other bidirectional circuits (e.g. EVSE with V2G) are correctly treated as load.
The difference column now compares imported kWh (what the user pays for) between Before and After, rather than net energy. A reduction in imports shows green, an increase shows red.
No description provided.