Repository navigation
minor: headless runs from a house spec, with a timeline and a record - #10
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4dedb73fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| s.redialAt = time.Now().Add(d) | ||
| conn := s.wsConn | ||
| s.mu.Unlock() | ||
| if conn != nil { | ||
| conn.Close() |
There was a problem hiding this comment.
Recheck the outage deadline before each reconnect attempt
If a reconnect event arrives while the charger is already waiting to retry a failed connection or an earlier drop, wsConn is nil and updating redialAt does not affect the existing timer. Both retry paths in clientLoop dial immediately after that timer expires without rechecking the deadline, so a requested 60-second outage can end after only a few seconds. Recheck redialAt before dialing, or wake and reschedule the pending wait when the deadline changes, so overlapping outages preserve the requested test conditions.
Useful? React with 👍 / 👎.
| if r.meter != nil { | ||
| add("meter", "sdm630", r.meter) |
There was a problem hiding this comment.
Reserve the site meter's device ID during spec validation
A spec containing an inverter or charger with "id": "meter" passes the device-ID validation, but this assignment silently replaces its entry in ports.json with the automatically created SDM630 meter. Events and plant samples still associate that ID with the original device, while an EMS reading the port manifest connects to a different device. Reject the reserved ID when building the spec, or give the site meter a collision-free identity.
Useful? React with 👍 / 👎.
b4dedb7 to
fc3604d
Compare
Each connection to a Central System started another heartbeat, MeterValues and simulation loop, and nothing ended them but Stop. After one reconnect a charger sent every heartbeat and MeterValues twice and integrated its energy twice a second. Heartbeats and MeterValues now end with their connection; the simulation runs once for the charger's life, connected or not, since a car charges through a network outage. Two hooks for scripted runs: DropConnection(d) closes the connection as a lost network does and keeps the charger away for d, with the session going on; SetFrameLog reports every OCPP-J frame the charger reads or writes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A car takes at most its own current per phase and charges on its own phases: a one-phase car under a three-phase order draws on L1 only, and a car that takes 10 A draws 10 A of a 16 A offer. The charger's offer (effectivePowerW) is unchanged; its draw follows the car. Co-Authored-By: Claude Opus 5.5 <[email protected]>
simulator run -spec house.json -port-base N -out DIR builds one house: devices by the device-drivers driver they emulate (a hybrid whose battery follows its EMS writes, OCPP chargers with their car), the site meter behind a fuse per phase, and the house load. It serves them on ports from the base, applies a timeline of events (loads, pv, soc, plug, car, offline, online, reconnect, mark, stop), and takes the same events over POST /api/run/event while it runs. It records a sample a second of what flows through each fuse, the fuse's damage and each device's state to plant.jsonl, every OCPP frame per charger, ports.json and result.json. Self-update endpoints are off in a run. The site loop read Running without the simulator's lock while a stop wrote it; it now takes the lock. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…" is reserved Codex's review of #10: - A reconnect event while the charger was already waiting to retry only moved a deadline nobody read; the retry dialled after the usual delay. Each dial now waits out DropConnection's deadline first. - A device called "meter" lost its ports.json entry to the site meter. The spec now refuses that id. Co-Authored-By: Claude Opus 5.5 <[email protected]>
freeBase took a base from the kernel's next ephemeral port and skipped those over 65000. macOS hands ephemeral ports out in turn, so once its cursor sat near the top every try was skipped and every run test failed with "no free port range". Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the thermal fuse on main (#9), a run's spec can set the cabinet's air (site.fuse_ambient_c, default 25 °C) and start the fuses warm (site.fuse_preheat_a, a current per phase they have carried long enough to settle), as a house is in the evening. A fuse size the profile does not model is refused before the run touches the simulator's settings. The site loop now reads its tick before it starts: a run sets the tick before it builds its site, and the next run could set it again while the last loop was still reading it (the race detector caught it once runs followed each other within milliseconds). Co-Authored-By: Claude Opus 5.5 <[email protected]>
fc3604d to
72044c0
Compare
3bf2996 to
6e5b87e
Compare
* feat(run): faults and seeded random timelines Faults for an EMS under test, as events in a headless run and over /api/run/event: a Modbus device that keeps its connections and answers nothing (silence), a site meter whose readings stand still (meter_late), one reading whose total disagrees with its phases (meter_glitch), phase currents that carry a sign (signed_currents), and a charger's own cap per phase (load_balancer). New model parameters, overridable per device: startup_delay_s (the car draws that long after an offer rises from zero) and load_balancer_a for an OCPP charger, signed_phase_currents for a meter. A spec's random block draws a timeline from a seed within the house's bounds (house load per phase under the fuse less 2 A), picks the open timing of each device, and writes timeline.json, a spec that replays it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(run): each session waits out the startup delay; faults keep the latest; a late meter keeps its energy Codex's review of #11: - offerSince now resets when a connector leaves its session, so every session waits out startup_delay_s, not only the first. - An earlier silence's or outage's timer no longer ends a later one: each device counts its faults and a timer acts only for the latest. - Energy that flows during meter_late shows on the counters when the readings move again; before, it was lost. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ocpp): a charger wired on one phase wired_phases: 1 in a run spec (SetWiredPhases) wires a charger on L1. It gives an order's current there whatever numberPhases says, and max_kw is its rating as wired, so a 16 A order on a 3.68 kW charger draws 16 A on L1. Before, the order became 16 A x 3 x 230 W, was capped to 3.68 kW and spread over three phases, and a one-phase car took 5.3 A of it. FTW's site test needs it for a charger wired on one phase, under Charge now and planned charging. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(ocpp): send Basic auth from user:password in the OCPP URL A Central System that requires OCPP 1.6 security profile 1 (FTW does) refused the simulated charger: it dialled without credentials, and the WebSocket dialer rejects user:password inside the URL. Move them to an Authorization header and keep the password out of the logs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ocpp): deliver StartTransaction once the charger is online A car plugged in before the charger had reached its Central System started a local session whose StartTransaction was lost: sendRequest failed and nothing kept the message. Queue StartTransaction and StopTransaction while there is no connection and send them, in order, after the next BootNotification is accepted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ocpp): profile_delay_s answers Accepted at once and applies later A cloud charger such as Easee accepts a charging profile at once and changes its output a minute or two later. The simulated charger applied every profile at once, so nothing could test how a Central System copes with that. POST /api/simulators/{id}/config {"profile_delay_s": 60} now holds accepted SetChargingProfile and ClearChargingProfile changes for that long, in order. Invalid profiles are still refused at once. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ocpp): a 1-phase profile loads L1, not a third of each phase The simulated charger always spread its draw evenly over three phases, also under a profile with numberPhases 1, so a site meter never saw the one-phase load a real car puts on L1. A charger that can switch phases (ConnectorSwitch3to1PhaseSupported) now draws a 1-phase profile on L1 only, at most its rated current per phase, and reports it per phase: phasePowers and per-phase currents in its state and MeterValues. The site meter takes a charger's draw per phase instead of through the site's phase factors. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(sungrow): write register defaults in engineering units Register defaults are written through each register's scale, but the Sungrow ones were raw register values: an SH10RT came up rated 100 kW (5000, 0.1 kW) with 500 kW charge and discharge limits (33046/33047, 0.01 kW). Use 10 kW and 5 kW, in the register set, the global defaults and the sungrow-hybrid and residential-5kw profiles. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(sungrow): ems_control makes the battery follow the EMS registers The Sungrow model stored an EMS's writes to 13049-13051 and never applied them, so an EMS under test commanded a battery that did not move. With POST /api/simulators/{id}/config {"ems_control": true} the battery does what a real SH does: in forced mode (13049 = 2) it follows 13050 (170 charge, 187 discharge, 204 stop) and 13051 watts; in any other mode it runs self-consumption against the site meter. It answers after ems_response_ms (default 2000), stays within 33046/33047 and the SoC limits 13057/13058, and writes the power-flow flags (13000) the Lua driver signs the power by. Off by default: the dashboard sets the battery by writing the registers directly. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(profiles): each model names its driver and carries its timing A profile now says which srcfl/device-drivers driver it emulates (driver_id, the file stem: sungrow, sdm630, pixii, ocpp...) and carries the model's timing in a model block, so field evidence for a driver can later set it. command_response_s replaces the two settings this branch added (ems_response_ms, profile_delay_s): it is how long the device takes to act on an accepted command, the battery after an EMS write or the charger after a charging profile. sungrow-hybrid says 2 s; applying a profile sets it, and POST /api/simulators/{id}/config overrides it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix: zero battery limits, phases in composite schedules, stop ids after a queue Codex's review of #8: - A Sungrow limit register holding 0 now allows no power that way; before, 0 turned the clamp off. - GetCompositeSchedule reports the phases the charger draws on: 16 A on one phase, not 5.3 A on three. A change of phases starts a new period. - A session that starts and ends offline is stopped under the transaction id the Central System gives its queued start. Co-Authored-By: Claude Opus 5.5 <[email protected]> * minor: headless runs from a house spec, with a timeline and a record (#10) * fix(ocpp): one heartbeat and meter loop per connection, DropConnection Each connection to a Central System started another heartbeat, MeterValues and simulation loop, and nothing ended them but Stop. After one reconnect a charger sent every heartbeat and MeterValues twice and integrated its energy twice a second. Heartbeats and MeterValues now end with their connection; the simulation runs once for the charger's life, connected or not, since a car charges through a network outage. Two hooks for scripted runs: DropConnection(d) closes the connection as a lost network does and keeps the charger away for d, with the session going on; SetFrameLog reports every OCPP-J frame the charger reads or writes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ocpp): the car's own limits (SetCar) A car takes at most its own current per phase and charges on its own phases: a one-phase car under a three-phase order draws on L1 only, and a car that takes 10 A draws 10 A of a 16 A offer. The charger's offer (effectivePowerW) is unchanged; its draw follows the car. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(run): headless runs from a house spec, with a timeline and a record simulator run -spec house.json -port-base N -out DIR builds one house: devices by the device-drivers driver they emulate (a hybrid whose battery follows its EMS writes, OCPP chargers with their car), the site meter behind a fuse per phase, and the house load. It serves them on ports from the base, applies a timeline of events (loads, pv, soc, plug, car, offline, online, reconnect, mark, stop), and takes the same events over POST /api/run/event while it runs. It records a sample a second of what flows through each fuse, the fuse's damage and each device's state to plant.jsonl, every OCPP frame per charger, ports.json and result.json. Self-update endpoints are off in a run. The site loop read Running without the simulator's lock while a stop wrote it; it now takes the lock. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(run): an outage asked for during a retry keeps its length; "meter" is reserved Codex's review of #10: - A reconnect event while the charger was already waiting to retry only moved a deadline nobody read; the retry dialled after the usual delay. Each dial now waits out DropConnection's deadline first. - A device called "meter" lost its ports.json entry to the site meter. The spec now refuses that id. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(run): draw port bases below the ephemeral range freeBase took a base from the kernel's next ephemeral port and skipped those over 65000. macOS hands ephemeral ports out in turn, so once its cursor sat near the top every try was skipped and every run test failed with "no free port range". Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(run): the fuse cabinet's air and a warm start With the thermal fuse on main (#9), a run's spec can set the cabinet's air (site.fuse_ambient_c, default 25 °C) and start the fuses warm (site.fuse_preheat_a, a current per phase they have carried long enough to settle), as a house is in the evening. A fuse size the profile does not model is refused before the run touches the simulator's settings. The site loop now reads its tick before it starts: a run sets the tick before it builds its site, and the next run could set it again while the last loop was still reading it (the race detector caught it once runs followed each other within milliseconds). Co-Authored-By: Claude Opus 5.5 <[email protected]> * minor: faults and seeded random timelines for headless runs (#11) * feat(run): faults and seeded random timelines Faults for an EMS under test, as events in a headless run and over /api/run/event: a Modbus device that keeps its connections and answers nothing (silence), a site meter whose readings stand still (meter_late), one reading whose total disagrees with its phases (meter_glitch), phase currents that carry a sign (signed_currents), and a charger's own cap per phase (load_balancer). New model parameters, overridable per device: startup_delay_s (the car draws that long after an offer rises from zero) and load_balancer_a for an OCPP charger, signed_phase_currents for a meter. A spec's random block draws a timeline from a seed within the house's bounds (house load per phase under the fuse less 2 A), picks the open timing of each device, and writes timeline.json, a spec that replays it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(run): each session waits out the startup delay; faults keep the latest; a late meter keeps its energy Codex's review of #11: - offerSince now resets when a connector leaves its session, so every session waits out startup_delay_s, not only the first. - An earlier silence's or outage's timer no longer ends a later one: each device counts its faults and a timer acts only for the latest. - Energy that flows during meter_late shows on the counters when the readings move again; before, it was lost. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ocpp): a charger wired on one phase wired_phases: 1 in a run spec (SetWiredPhases) wires a charger on L1. It gives an order's current there whatever numberPhases says, and max_kw is its rating as wired, so a 16 A order on a 3.68 kW charger draws 16 A on L1. Before, the order became 16 A x 3 x 230 W, was capped to 3.68 kW and spread over three phases, and a one-phase car took 5.3 A of it. FTW's site test needs it for a charger wired on one phase, under Charge now and planned charging. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Stacked on #8 (base
fix/ftw-rig-gaps); retarget tomainonce #8 merges. Third of the PRs described in #8's "split" table.simulator run -spec house.json -port-base N -out DIRbuilds one house from a JSON spec, serves it on ports from the base, applies a timeline of events and records the house once a second. An EMS's test harness talks to the house over Modbus and OCPP like a real site, reads the record and can post events while the house runs. FTW's site test (srcfl/ftw#1797,go/test/site) uses it in place of its own house builder, recorder and OCPP proxy. Full reference: docs/HEADLESS_RUN.md.What it adds
runSpec): the site (fuse rating and profile, house load per phase), devices by the device-drivers driver they emulate (sungrow, orocppfor a charger withocpp_url, rating, phase switching, OCPP configuration and the car's current and phases), and a timeline. The site meter (SDM630) comes with the site. A device's timing comes from its model and can be overridden (command_response_s).ports.jsonsays where each device is.POST /api/run/eventwhile the house runs:mark,loads,pv,soc,plug,car,offline/online,reconnect,stop;POST /api/run/markfor a named moment.plant.jsonl(a sample a second of what flows through each fuse, the fuse's damage, the meter's reading and each device's state, plus every event and mark),ocpp-<id>.jsonl(every frame a charger reads or writes, from the charger itself) andresult.json.Fixes it needed
Stopended: after one reconnect a charger sent everything twice and integrated its energy twice a second. Heartbeats and MeterValues now end with their connection; the simulation runs once for the charger's life, connected or not.SetCar): a one-phase car under a three-phase order draws on L1 only; a car that takes 10 A takes 10 A of a 16 A offer.DropConnection(d)closes a charger's connection as a lost network does and keeps it away ford, with the session going on;SetFrameLogreports every OCPP-J frame.Runningwithout the simulator's lock while a stop wrote it (race detector); it now takes the lock.Tests
TestRunRecordsTheHouse(spec, timeline, record, ports, result, nothing left behind),TestRunTakesEventsOverHTTP,TestRunChargerReconnects(Basic auth, a one-phase car, a reconnect that keeps the session, the frame log),TestDropConnectionKeepsTheSessionAndOneLoopSet(one heartbeat per interval after two drops; it fails before the loop fix),TestCarTakesWhatItCan.go test -race ./...passes.End to end: FTW's
s1-25(Charge now, 25 A) through this runner gives 10 972 W after 2.2 s and nofuse_limit, the same as the old rig's 10 971 W after 2.1 s;s6,s9,s11and the news9rpass, and the two new cases that FTW expects to fail on master (s5t,s13) fail as expected.Review fixes (Codex, b4dedb7)
reconnectwhile a retry was already waiting ended after the usual retry delayTestDropConnectionWhileRetrying(fails before the fix: back after 42 ms of 600)meterlost itsports.jsonentry to the site meterTestRunRefusesTheMeterIDAlso
freeBasein the tests drew a base from the kernel's next ephemeral port and skipped those over 65000; once macOS's cursor sat near the top, every run test failed with "no free port range". It now draws bases from 20000-49900.The thermal fuse (23 October)
Rebased on
mainwith #9, so a run's site has the thermal fuse.site.fuse_ambient_csets the air around the fuses (default 25 °C).site.fuse_preheat_ais a current per phase the fuses have carried long enough to settle, so a scenario that starts in the evening starts warm.damagemeans. Thefuse.damagefield inplant.jsonlis now the element's way from ambient to melting, 0 to 1.TestRunFuseStartsWarmcovers the warm and cold starts and the refused 10 A.🤖 Generated with Claude Code