feat: external_solar_mode for AC-coupled PV setups - #167
Conversation
On AC-coupled installations the PV panels are wired to a separate inverter (e.g. SolarEdge, microinverters) and the battery inverter has no DC solar input. The only physical charging path is via the grid — surplus solar returns through the meter. With SOLAR_STORAGE hard-coded to grid_charge=False the battery sits idle the entire solar window. Adds an opt-in battery.external_solar_mode flag (default false, so DC-coupled users see no change). When enabled, the SOLAR_STORAGE intent maps to grid_charge=True in the inverter controller; all other intents keep their default mapping. Wired end-to-end: - BatterySettings dataclass + from_ha_config - InverterController helper applied in _map_intent_to_rates, get_period_settings, and get_detailed_period_groups - settings_store bootstrap defaults + schema migration - Settings → Battery tab toggle (PV coupling section) - Setup wizard load + complete payload Tests: 10 new behavioral tests covering the override in isolation and through get_period_settings / get_detailed_period_groups. Full unit suite (600) and backend suite (201) pass. Closes johanzander#162
Combines two pending upstream PRs into a local fork build so the AC-coupled installation can use them before they land on johanzander/main: - johanzander#164: extend Nordpool area hints to NL/BE/DE/FR/AT/PL - johanzander#167: external_solar_mode for AC-coupled PV setups
|
Thanks for this — the feature is well-structured and the end-to-end wiring (dataclass, store migration, API, wizard, Settings page) is clean. We'd be happy to accept it, but we need the fix to work correctly across all supported inverter types before it lands. The logic layer is fully unit-testable without real hardware, so no AC-coupled device is required to close the gaps. Required before merge1.
|
|
Great work Johan, and very nice to have the Modbus via solax nowadays. 🙏 I made it in a fork before but you made it nicer 😉 |
Upstream has merged PR johanzander#164 (Nordpool continental areas), so the fork now carries only the still-pending PR johanzander#167 (external_solar_mode). Fork build wiring (image:, workflow registry owner, workflow_dispatch) is reapplied on top of upstream 9.6.2.
Bundle all jvdd-fork changes accumulated since rebase on upstream 9.6.2
under one minor-version label. No code changes vs jvdd.7:
Features:
- external_solar_mode (upstream PR johanzander#167, pending)
Fixes:
- AI Analyst model IDs updated + persisted-config auto-migration (PR johanzander#180)
- SolaxModbus TOU begin/end write via time.* entity mirror (issue johanzander#181)
|
What do you need for this?, it's working at my end. Is it different for other inverters? I thought it's a manual override for the schedule and sets the ac charge switch on modbus or api. |
|
I would need the review comments addressed and all test to pass. |
On AC-coupled setups, switching grid_charge to True alone is not enough: with the TOU slot in Load First mode, the inverter's EMS does not actively initiate charging. The slot mode also needs to switch to Battery First, which makes the inverter actively pull power from the AC side during the planned solar window. This commit: - Adds _effective_mode_for_intent() mirroring _effective_grid_charge(): returns 'battery_first' for SOLAR_STORAGE when external_solar_mode is enabled, otherwise the default mode. - Applies it in inverter_controller.get_period_settings and get_detailed_period_groups (display paths), and in the three places the SolaxModbusGrowattController computes mode from intent. - Adds 6 tests covering the override on SOLAR_STORAGE, default behaviour when disabled, no leakage to other intents, and propagation through get_detailed_period_groups. Trade-off documented in the helper docstring: Battery First charges at the configured rate regardless of actual solar surplus, so in a SOLAR_STORAGE period with insufficient forecast accuracy the battery will draw from grid. BESS only plans SOLAR_STORAGE when surplus is expected, so the exposure is bounded by forecast quality. A future follow-up could rate-limit the EMS charging rate to match measured solar export, but that requires sensor data BESS does not currently track at this granularity. Live-verified on a Growatt MID 15KTL3-XH (SolaxModbus integration): without this change SOLAR_STORAGE periods produced no battery action; with this change battery charges actively during planned SOLAR_STORAGE hours.
|
Follow-up commit on this branch: also override the battery mode (not just Why this matters in practice: I went live-testing the previous version of this PR (jdungen fork build) on a Growatt MID 15KTL3-XH (SolaxModbus integration). Even with The change:
Documented trade-off in the helper docstring: Battery First charges at the configured rate regardless of actual solar surplus. If the forecast over-estimates solar in a SOLAR_STORAGE window, the inverter will pull from grid. The risk is bounded by forecast accuracy and the fact that BESS only plans SOLAR_STORAGE when surplus is expected. A future follow-up could rate-limit the EMS charging rate to match measured solar export, but that needs sensor data we don't currently track at this granularity. I'm running this on my fork build now and will report back on whether real-world battery behaviour matches the planned SOLAR_STORAGE periods over the next sunny day or two. |
Upstream merged our PR johanzander#180 (AI Analyst model IDs) in 9.6.3, so that patch is dropped from the fork diff. The fork now carries: - external_solar_mode (PR johanzander#167, still pending) — now with mode override in addition to grid_charge override - SolaxModbus TOU begin/end via time.* entity mirror (issue johanzander#181) Bumped to 9.6.4-jvdd.1 to stay above upstream's 9.6.3 release.
|
I am doing some fundamental changes to the algorithm and intent modes here: #187, that probably affects this PR. Lets follow up this one, after it has been merged and released. |
|
PR #187 has now merged, so this is ready to move forward. You'll need to rebase onto One thing the rebase needs to handle: #187 introduced a passive solar charging path. IDLE periods where the optimizer chose The intent classification and passive charging model are documented in Also curious to hear your real-world results — did battery behaviour match the planned SOLAR_STORAGE periods? |

Summary
battery.external_solar_mode(defaultfalse) so AC-coupled installations can charge during solar hours.SOLAR_STORAGEperiods map togrid_charge=Truein the inverter controller; all other intents keep their default mapping.false).Closes #162.
Why
On AC-coupled installations (e.g. SolarEdge for PV + Growatt for battery, microinverters, or any external-inverter setup) the battery inverter has no DC solar input. Surplus solar reaches the battery only via the meter. Today inverter_controller.py:35 hard-codes
SOLAR_STORAGEtogrid_charge=False, so the battery sits idle the entire solar window even though the DP planner has scheduled storage.Files
core/bess/settings.py—external_solar_mode: bool = FalseonBatterySettingscore/bess/inverter_controller.py—_effective_grid_chargehelper applied in_map_intent_to_rates,get_period_settings, andget_detailed_period_groupsbackend/settings_store.py— bootstrap default + schema-migration entrybackend/api.py—_BATTERY_MAP+ live-update payload for/api/setup/completebackend/api_dataclasses.py—externalSolarModeonAPISetupCompletePayloadfrontend/src/components/settings/BatteryFormSection.tsx— new "PV coupling" section with togglefrontend/src/pages/SettingsPage.tsx,SetupWizardPage.tsx— wire load/savefrontend/src/types.ts— optionalexternalSolarModeTest plan
pytest core/bess/tests/unit/test_external_solar_mode.py— 10 new tests pass (default-disabled, SOLAR_STORAGE override on/off, other intents unaffected, get_period_settings + get_detailed_period_groups apply override)pytest core/bess/tests/unit/ -m "not slow"— 600 passed, 12 skippedpytest backend/tests/ -m "not slow"— 201 passed