Skip to content

Charge Manager tests in divert_sim, and a downloadable replay package - #1351

Draft
jeremypoulter wants to merge 6 commits into
masterfrom
claude/relaxed-ramanujan-tij76b
Draft

jeremypoulter wants to merge 6 commits into
masterfrom
claude/relaxed-ramanujan-tij76b

Conversation

@jeremypoulter

Copy link
Copy Markdown
Collaborator

Two related additions:

  • the simulator now tests every Charge Manager feature, on its own and in combination;
  • a charger can hand over a "replay package" that the simulator plays back through the same firmware logic.

Charge Manager in the simulator

Each simulated charge point now runs the firmware's Scheduler, Limit, TempThrottleTask and RfidTask (fed by a simulated card reader). These sit alongside divert, the shaper, manual override and boost, which it already ran. Scheduled rules, session limits, temperature protection and RFID gating therefore execute real firmware code.

Firmware changes needed for this (small):

  • Scheduler takes its divert, shaper, RFID and limit collaborators by reference instead of using the globals (main.cpp: Scheduler scheduler(evse, divert, shaper, rfid, limit)). That lets each simulated peer own one.
  • MQTT publishing and the SNTP time-change hook are compiled out under DIVERT_SIM. In the sim, the scheduler reads simulated time.
  • rfid.cpp drops two unused includes.

Simulator changes:

  • The RAPI shim reports a protocol 5 controller with vflags, the simulated temperature on $GP, and the station current ($GC cmaxamps, $SC M).
  • New scenario keys:
    • a schedule;
    • a temperature input;
    • peer options rfid_reader, ev.report_soc and live_pwr_add_ev;
    • events rfid, temperature, soc, limit, schedule, claim and release. claim/release cover sources the sim doesn't run, such as OCPP, MQTT and evcc.
  • New CSV columns: pilot_a, temperature_c, limit, rfid_auth, schedule_event and session_wh.
  • Each scenario run gets a private EpoxyFS/EEPROM directory, so parallel runs no longer share persisted state.

Tests:

  • charge_manager.py configures the station the way the Charge Manager screen does. Scheduled rules go through a port of the GUI's rulesToTimers. It also provides an oracle that checks each row against the winning claims.
  • test_charge_manager.py (31 tests) covers each feature on its own: station defaults, every always-on feature, temperature protection, every scheduled rule action, rule current/limit, midnight wrap, day filters, manual override, and editing or deleting a live rule.
  • test_charge_manager_combinations.py runs all 417 combinations the Charge Manager allows through one scripted day. On every row it checks claim arbitration and each feature's contract. Breaking RFID gating or the divert timer priority in the firmware makes 244 of them fail.

Gaps found, kept as strict xfails:

  • A scheduled Disable rule with a stop time never ends. rulesToTimers (gui-nightshift) writes its stop as another disabled timer.
  • A scheduled OCPP rule is a plain charge window, because SchedulerFeature::OCPP is a placeholder.

Replay package

The recorder is passive: it polls public getters about once a second and drives nothing.

  • Every 10 s for the last hour it samples solar, grid and site power, voltage, temperature, plug state, EVSE state, pilot, current and target.
  • It also logs every claim, limit, boost, RFID, schedule and config change.

The rings take about 12 KB of heap, allocated at boot. Build flags size them: ENABLE_REPLAY_RECORDER, REPLAY_SAMPLES, REPLAY_SAMPLE_INTERVAL_MS and REPLAY_EVENTS.

Download: GET /debug/replay returns the package (add ?download=1 to save it as a file).

  • The config is an allowlist of behaviour settings (replay_redact.cpp), the same approach as crash reports. Stored RFID tags become numbered placeholders.
  • Mongoose buffers the response in one heap block. The package is therefore sized to the largest free block, keeps the newest data that fits, and reports how much it left out.

Replay: divert_sim/replay.py package.json turns a package into a scenario. The scenario carries:

  • the charger's config and schedule;
  • the measured inputs;
  • plug-in/out and EV-pause events;
  • RFID authorisations as card taps;
  • user limits and boosts;
  • the recorded claims of clients the sim doesn't run.

The tool runs that scenario and reports where the simulated charger differs from the real one.

Docs: api.yml, the feature map, troubleshooting.md and the divert_sim README are updated.

Testing

  • divert_sim pytest: 542 passed, 2 xfailed (the gaps above).
  • native_test: all doctest suites pass, including the new test_replay_format (12 cases: ring, encoding, claim diff, redaction).
  • Builds: native_simulator, native_openevse and openevse_wifi_v1 all build. On openevse_wifi_v1, flash is 91.0% and static RAM 25.6%.
  • End to end: I ran the native firmware against the OpenEVSE emulator and drove an eco/solar session with a manual override through the real HTTP API. Replaying the downloaded package matched the device's charging state on 100% of samples, with the pilot off by 0.8 A on average. That capture is committed as divert_sim/data/replay/native_eco_manual_override.json and replayed by test_replay.py.
  • tests/integration/test_replay.py passed (6/6) against that native + emulator pair, using a local stand-in fixture. I could not run it through the docker harness here, so CI will be its first run there.

Not in this PR

  • No download button in the GUI yet. That change belongs in gui-nightshift; for now /debug/replay?download=1 works as a direct link.
  • I haven't checked on real hardware how much of the hour a 4 MB board without PSRAM can send.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XEyVzUHFzxthd54TWTGA1r


Generated by Claude Code

claude added 3 commits October 7, 2026 19:14
The simulator now builds the firmware's Scheduler, Limit, TempThrottleTask
and RfidTask into every peer, so Charge Manager rules, session limits,
temperature protection and RFID gating execute the real firmware code.

- Scheduler takes its divert/shaper/RFID/limit collaborators by reference
  instead of reaching for the globals, so each peer can own one. MQTT
  publishing and the SNTP time-change hook are compiled out under
  DIVERT_SIM, where the scheduler reads simulated wall-clock time.
- rfid.cpp drops two unused includes (mqtt.h, input.h).
- The RAPI shim reports a protocol 5 controller with vflags (EV connected,
  charging) and the simulated EVSE temperature on $GP.
- Scenarios gain a schedule (scenario-wide or per peer), a temperature
  input, rfid_reader/report_soc peer options and rfid, temperature, soc,
  claim, release, limit and schedule events.
- CSV output adds pilot_a, temperature_c, limit, rfid_auth and
  schedule_event per peer.
- Scenario runs use a private EpoxyFS/EEPROM directory, so parallel runs
  no longer share persisted state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEyVzUHFzxthd54TWTGA1r
charge_manager.py configures a simulated station the way the Charge
Manager screen does (always-on features as config, scheduled rules
compiled with a port of the GUI's rulesToTimers) and parses the claim
columns into an arbitration oracle.

test_charge_manager.py covers each feature on its own: default state and
station current, session limits (energy, time, SoC), eco divert, grid
shaping, RFID, OCPP (as the claims OcppTask makes), temperature
protection, every scheduled rule action, rule current and limit, midnight
wrap, day filters, rules without a stop, manual override, and live rule
edits and deletes.

test_charge_manager_combinations.py runs all 417 combinations the Charge
Manager allows through one scripted day and checks claim arbitration and
each feature's contract on every row. Breaking RFID gating or the divert
timer priority in the firmware fails 244 of these tests.

Two known gaps are strict xfails:
- a scheduled OCPP rule is a plain charge window (SchedulerFeature::OCPP
  is a placeholder);
- a timed Disable rule never ends, because rulesToTimers writes its stop
  as another 'disabled' timer.

Simulator fidelity fixes found along the way:
- the RAPI shim holds the station current ($GC cmaxamps, $SC M), so
  max_current_soft caps the pilot;
- live_pwr_add_ev models a whole-site meter that also sees the EV, taking
  the EV's share from the firmware's own ammeter reading;
- a session_wh column exposes the firmware's session energy;
- run_scenario_doc runs an in-memory scenario.

Five charge_manager_* showcase scenarios are generated for the viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEyVzUHFzxthd54TWTGA1r
…n divert_sim

The firmware now keeps a replay recording: every 10 s the solar, grid and
site power, voltage, temperature, plug state, EVSE state, pilot, current
and target, plus every claim, limit, boost, RFID, schedule and config
change. GET /debug/replay (?download=1 to save) returns it with the
configuration and schedule. The recorder is passive (it polls public
getters) and allocates ~12 KB once at boot; ENABLE_REPLAY_RECORDER,
REPLAY_SAMPLES, REPLAY_SAMPLE_INTERVAL_MS and REPLAY_EVENTS size it.

- replay_redact.cpp: the package configuration is an allowlist of
  behaviour settings, as for crash reports; stored RFID tags become
  numbered placeholders.
- The response is buffered in one heap block, so the package is sized to
  the largest free block and keeps the newest samples and events that fit,
  reporting how many it left out.
- divert_sim/replay.py converts a package into a scenario (config,
  schedule, measured inputs, plug and EV-pause events, card taps, user
  limits and boosts, and the recorded claims of clients the simulator does
  not run), runs it, and reports where the simulation differs from the
  device.
- A real capture from the native build against the emulator replays at
  100% charging-state agreement (divert_sim/test_replay.py).
- Native unit tests (test/test_replay_format) cover the ring, sample
  encoding, claim diffing and redaction; tests/integration/test_replay.py
  covers the endpoint.
- api.yml, the feature map, troubleshooting.md and the divert_sim README
  document it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEyVzUHFzxthd54TWTGA1r
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Unit Tests

   26 files  + 1     26 suites  +1   0s ⏱️ ±0s
  220 tests +12    220 ✅ +12  0 💤 ±0  0 ❌ ±0 
1 746 runs  +65  1 746 ✅ +65  0 💤 ±0  0 ❌ ±0 

Results for commit e5cad20. ± Comparison against base commit 70071cb.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Simulator Tests

544 tests  +463   542 ✅ +461   5m 26s ⏱️ + 3m 59s
  1 suites ±  0     2 💤 +  2 
  1 files   ±  0     0 ❌ ±  0 

Results for commit e5cad20. ± Comparison against base commit 70071cb.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Integration Tests

  1 files    1 suites   7m 5s ⏱️
112 tests 111 ✅ 1 💤 0 ❌
118 runs  117 ✅ 1 💤 0 ❌

Results for commit e5cad20.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Flash usage

Environment Size Flash used Change vs base
adafruit_featheresp32 1,786,137 B 90.8% +9,220 B (+0.40 pp)
adafruit_huzzah32 1,737,249 B 88.4% +9,124 B (+0.50 pp)
adafruit_huzzah32_dev 1,785,357 B 90.8% +9,168 B (+0.50 pp)
esp32-c3-devkitc-02 1,846,152 B 93.9% +10,152 B (+0.50 pp)
espressif_esp-wrover-kit 1,802,101 B 91.7% +9,360 B (+0.50 pp)
espressif_esp-wrover-kit_latest 1,802,437 B 91.7% +9,360 B (+0.50 pp)
heltec_esp32-wifi-lora-v2 1,737,653 B 88.4% +9,100 B (+0.50 pp)
nodemcu-32s 1,784,921 B 90.8% +9,168 B (+0.50 pp)
olimex_esp32-gateway-e 1,775,733 B 90.3% +9,044 B (+0.40 pp)
olimex_esp32-gateway-e_dev 2,979,317 B 75.8% +33,380 B (+0.90 pp)
olimex_esp32-gateway-f 1,775,733 B 90.3% +9,044 B (+0.40 pp)
olimex_esp32-gateway-f_dev 2,979,317 B 75.8% +33,380 B (+0.90 pp)
olimex_esp32-gateway-old 1,775,813 B 90.3% +9,056 B (+0.40 pp)
olimex_esp32-poe-iso 1,775,473 B 90.3% +9,084 B (+0.50 pp)
openevse_wifi_tft_v1 2,564,211 B 39.1% +13,844 B (+0.20 pp)
openevse_wifi_tft_v1_dev 2,862,537 B 43.7% +11,532 B (+0.20 pp)
openevse_wifi_v1 1,832,477 B 93.2% +9,656 B (+0.50 pp)
openevse_wifi_v1_16mb 2,153,235 B 32.9% +13,392 B (+0.20 pp)
wt32-eth01 1,775,173 B 90.3% +9,060 B (+0.50 pp)

Builds at or above 95% of their app partition are flagged ⚠️; builds that exceed it (🚨) fail the build.

claude and others added 2 commits October 16, 2026 17:07
The ArduinoMongoose v7 migration (#1155) dropped the $ anchor from every
route; under v7 "/debug/replay$" only matches that literal path, so the
merge left the replay endpoint answering 404 and the integration tests
failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEyVzUHFzxthd54TWTGA1r
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants