Repository navigation
Conversation
Some tariffs net import against export within a fixed settlement window (e.g. each clock hour) and only bill the net direction. The new apps.yaml setting metric_net_settlement_window_minutes (0/absent = off, unchanged behaviour) nets the money side of both prediction engines per window, nets today_cost per window, and seeds the current window from what has already been metered. Kernel ABI 8, parity 17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…assumed gone Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pps.yaml - Move metric_net_settlement_window_minutes parsing into utils.net_settlement_window_from_arg and Fetch.fetch_net_settlement_config. Read the raw value (no typed get_arg default), so 60.5 is rejected instead of being silently truncated to a valid-looking 60. - Log the enabled/invalid message once per change of the setting instead of every cycle. - Add tests/test_net_settlement_config.py (parsing, logging on change) and check that reset_sample_state drops net_settlement_seed. - Rename the net45_not_divisor parity case: 45 does divide 1440. - List the setting in docs/apps-yaml.md and document the plan-slot display, Energy Comparison and higher-cost cases in docs/energy-rates.md. - Update the debug dump tuple comment for net_settlement_seed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015uJe864TG4twBVZ1dNdN9K
…eed fields - An invalid metric_net_settlement_window_minutes now calls record_status(had_errors=True) every cycle, like other configuration errors, as had_errors is reset each run. The log line stays once per change. - The current-window seed is a NetSettlementSeed namedtuple (window, import_kwh, import_cost, export_kwh, export_credit, applied) instead of a bare 6-tuple. net_settlement_seed_from() rebuilds it from the plain list a debug dump replays, and the debug YAML dumper writes it as a list so dumps still load with yaml.safe_load. - Tests: status reporting on every invalid cycle, seed conversion, the debug dump round trip, and today_cost returning a NetSettlementSeed. - Docs: explain netting as import and export kWh cancelling each other out within the window; note that per-slot rate heuristics (iBoost, car charging slots) still use gross rates; say that predbat.cost_today is netted while cost_today_import/export stay gross. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015uJe864TG4twBVZ1dNdN9K
Built with build_kernel_cross.sh and zig 0.16.0 (the version the workflow pins). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
piomar123
marked this pull request as ready for review
October 7, 2026 15:37
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Some tariffs don't bill every import and export separately. They net import against export within a fixed settlement window (each clock hour, or quarter hour) and only charge or pay for the net direction. The hourly balancing used for households with their own generation in Poland works this way. Predbat currently prices every 5-minute step independently, so on these tariffs it overvalues same-hour arbitrage and misreports cost.
This PR adds an opt-in
apps.yamlsetting:When it's set, import and export in the same wall-clock window are netted before being priced. The net import is charged at the import rate, or the net export paid at the export rate. If a rate changes within the window, the winning direction's energy-weighted average rate is used. With the setting off, behaviour is bit-for-bit unchanged (see Testing).
What changes
prediction.pyhot loop andprediction_kernel.cpp) accumulate the window's import kWh/cost and export kWh/credit. They movemetricto the window's settled value at every step, so every snapshot (final_metricatend_record,metric_time) is consistent. The setting is off by default, and when off the original pricing lines are executed unchanged.metric_keepstay physical. Export that bypasses the CT clamp into a car (car_energy_reported_load: false) is excluded from the netting, consistent with it already earning no export credit. The PV10 correction for an Intelligent dispatch assumed gone (the car's import priced at the rate nominal pays, notrate_max) is applied to the step's import cost before it enters the window, so it is netted like any other import.today_cost()nets the metered day per window, socost_today_sofarandpredbat.cost_todaymatch a netted bill. A new attribute,cost_net_settlement_adjust, shows the difference from gross;cost_import/cost_exportstay gross.predbat.cost_houris a rolling 60 minutes spanning two windows, so it deliberately stays gross.today_cost()hands the in-progress window's metered totals to the prediction asnet_settlement_seed(aNetSettlementSeednamedtuple; the debug dump writes it as a plain list andnet_settlement_seed_from()rebuilds it on replay), so the forecast part of the hour nets against what has already happened. Both engines accept the seed only when its window id matchesminutes_now // window. The seed is cleared in the paths that zerocost_today_sofar:compare.py,annual.pyreset_sample_stateand the yesterday simulation inoutput.py.fetch_sensor_data()also resets it every cycle.Fetch.fetch_net_settlement_config(),utils.net_settlement_window_from_arg()): the window must be a multiple ofPREDICT_STEPthat divides 1440, so the plan's windows (which run on past midnight) andtoday_cost's (which restart at midnight) line up. The raw value is validated, so60.5is rejected rather than truncated to a valid-looking 60. Anything invalid disables netting instead of raising: theWarn:is logged once per change of the setting, andpredbat.statusis flagged (had_errors) every cycle, like other configuration errors. The key is also added toAPPS_SCHEMA.utils.net_settlement_value(), mirrored by the inlinepk_net_settlement_value()in the kernel.PkContextgainsmetric_net_settlement_windowand thenet_seed_*fields at the end.docs/energy-rates.md(including the plan-slot display, Energy Comparison, and the per-slot rate heuristics such as iBoost and car slots that still use gross rates), the setting listed indocs/apps-yaml.md, andcost_todayvs the grosscost_today_import/cost_today_exportindocs/output-data.md.Kernel binaries
This PR comes from a fork, and the
kernel-binariesjob only runs for branches in this repository. So the sixprediction_kernel_lib_*.sofiles are rebuilt in a separate commit. They were built withbuild_kernel_cross.shand the same zig version the workflow pins (0.16.0), with no other changes. Feel free to drop that commit and let CI rebuild them if you prefer.Testing
New
tests/test_net_settlement_config.py: parsing and validation of the setting, logging once per change,predbat.statusflagged on every invalid cycle, seed conversion, the debug dump round trip, andreset_sample_statedropping the seed.Test additions in
tests/test_kernel_parity.py, all wired intorun_edge_case_testssoverify_kernel_binary.pyruns them:net_settlement_value:final_metricmoves.today_cost(): netted day cost for 15/30/60-minute windows matches a hand calculation; window 0 stays exactly gross; the seed equals the current window's metered totals; there's no seed at the top of the hour.end_recordfalling mid-window, andend_record = 0;metric_keep;SCENARIO_STATE_ATTRS(which now includes the two new attributes), so later suites see the same fixture state as before.Results:
main+ patch), aarch64 (Raspberry Pi 4) in thepredbat_addonimage, the same way CI runs it:unit_test.py --quickwithPREDBAT_KERNEL_REQUIRED=1passed all 333 test groups, andverify_kernel_binary.pypassed on the rebuiltprediction_kernel_lib_aarch64.so. I couldn't run the x86_64 binary locally; CI checks that one.main(with its shipped aarch64 kernel) and through this branch with the window at 0. The dumped result tuples, including per-step SoC, were byte-identical on all three paths: Python engine,save="best"(includingmetric_time) and kernel. As a control, the same run with the window at 60 differs in 420-427 of the 450 scenarios, depending on the path. (The first version of this patch passed the same check against v9.1.0, plus a full./run_allwith identical pass/fail, 377/377, with and without it.)today_costadjustment, makes the new tests fail. So does netting the gross import cost instead of the car-gone-corrected one (net60_car_dispatch_gonefails parity).Known limitations
CLAUDE.md. Instead I checked the blast radius manually: allrun_predictioncallers (plan.py,compare.py,marginal.py,output.py,annual.py) and every site that zeroescost_today_sofar.🤖 Generated with Claude Code