Home Assistant custom integration that optimizes home battery charge/discharge scheduling using dynamic programming (backward induction) to minimize electricity costs.
- Tests:
python -m pytest tests/ -v - Type check:
mypy custom_components/battery_controller/ - Lint:
pre-commit run --all-files - Install deps:
pip install -r requirements.txt
custom_components/battery_controller/— integration codeoptimizer.py— DP engine (backward induction, Bellman equation)coordinator.py— 3 cascading coordinators (Weather→Forecast→Optimization)zero_grid_controller.py— real-time ~5s grid balance controlforecast_models.py— PV and consumption forecasting + historical price modelbattery_model.py— battery physics (RTE split as √RTE per direction)config_flow.py— setup + options flow withsection()helpersconst.py— SOC_RESOLUTION_WH=10, POWER_STEP_W=100 (boundary actions handle residual capacity), all config keys
tests/— pytest withpytest-homeassistant-custom-component+ syrupy snapshots
_attr_has_entity_name = Trueon all entities_attr_translation_keyalongside_attr_name(fallback)strings.json= copy oftranslations/en.json(required by HA)section()imported fromhomeassistant.data_entry_flow(not helpers.selector)- Use
async deffor all I/O; never synchronous I/O in async context
- Feed-in price: Never return
Noneto optimizer — falls back toCONF_FIXED_FEED_IN_PRICE(€0.07). Returning None causes DP to default to grid_price, making PV arbitrage unprofitable. - RTE split:
charge_eff = discharge_eff = sqrt(RTE)— ensurescharge × discharge = RTE - Oscillation filter: Post-DP pass removes charge↔discharge pairs where spread <
(2×degradation + min_price_spread) / sqrt(RTE). Uses 2-hour lookahead window. - DC PV: ~97% efficient (MPPT only); AC PV ~85% (through inverter). Excess DC goes to AC at 96%.
- SoC fallback: If SoC sensor unavailable, last known SoC from previous run is used.
Always do the following after modifying any source file:
- Run tests:
python -m pytest tests/ -v— fix any failures before proceeding - Run pre-commit:
pre-commit run --all-files— fix any issues (ruff may auto-format) - Update
docs/algorithm.md— if algorithm logic, efficiency model, or DP behaviour changed - Update
simulate_diagnostics.py— if inputs, outputs, or data structures changed - Update
docs/analyzer/index.html— if the simulator UI or displayed parameters changed
There are three DP implementations that must always be kept identical in algorithm, constants, and cost semantics:
custom_components/battery_controller/optimizer.py— the integration (source of truth)docs/analyzer/analyzer.js— JS re-implementation used by the diagnostic simulatorsimulate/simulate_diagnostics.py— Python script for local diagnostics testing
When changing any of the following in optimizer.py, apply the same change to analyzer.js and simulate_diagnostics.py:
- SoC transition logic (charge/discharge/idle, RTE split, passive DC PV)
calculate_step_costcost formula (grid cost, degradation, DC PV path, grid cap)- Action generation (power steps, charge/discharge limits)
- Terminal condition (terminal price, V[T] initialization)
- V[t][s] fallback for unreachable states
- Shadow price formula and sign convention
- Oscillation filter thresholds (min_price_spread, degradation formula)
- Constants:
SOC_RESOLUTION_WH,POWER_STEP_W,DC_TO_AC_INVERTER_EFFICIENCY
- Modified files and their key changes
- Test failure messages (full traceback)
- Current domain name (
battery_controller) and version from manifest.json