Skip to content

minor: faults and seeded random timelines for headless runs - #11

Merged
frahlg merged 3 commits into
feat/headless-runfrom
feat/faults-random
Oct 23, 2026
Merged

frahlg merged 3 commits into
feat/headless-runfrom
feat/faults-random

Conversation

@frahlg

@frahlg frahlg commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Stacked on #10 (base feat/headless-run); retarget once #10 merges. The last row of #8's "split" table.

Faults and quirks an EMS meets in the field, for headless runs, and seeded random timelines that mix them. FTW's site test uses them for its random mode: a timeline per seed, FTW's invariants checked every second, a failing seed shrunk and kept as a fixed case.

Faults

Events on a run's timeline or over POST /api/run/event:

Event What an EMS sees
silence (device, for_s) a Modbus device keeps its connections and answers nothing: timeouts, not a refused connection (modbus.Server.SetSilent)
meter_late (for_s) the site meter's readings stand still while the house goes on
meter_glitch one reading whose total reads 0 W while its phases are right, as a Pixii's total and phases sometimes disagree
signed_currents (on) the meter's phase currents carry the sign of their power (an SDM630 reports magnitudes)
load_balancer (device, max_a) an OCPP charger's own cap per phase, whatever it is offered (Easee's load balancer)

Model parameters

Per model in its profile's model block, overridable per device in a spec, so field evidence for a driver can set them later: startup_delay_s (the car draws that long after an offer rises from zero, while the charger reports Charging), load_balancer_a (OCPP charger) and signed_phase_currents (meter), beside #8's command_response_s.

A charger's wiring is set per device: wired_phases: 1 wires it 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 (TestOnePhaseWiring, TestRunWiresAOnePhaseCharger). Before, the order became 16 A × 3 × 230 W, was capped to 3.68 kW and spread over three phases.

Random timelines

A spec's random block (seed, duration_s, gap_s, optional kinds) draws events from the seed within the house's bounds: house load steps, drift and one-phase spikes (each phase stays under the fuse less 2 A, so the house alone never overloads it), PV cloud edges, the battery near its floor, the car taking less than offered, device silence and outages, charger reconnects, a late or glitched meter, signed currents and a load-balancer cap. Where the spec leaves them open it also picks each charger's command_response_s (0, 5, 40 or 60 s) and startup_delay_s, a one-phase car now and then, and the meter's sign. The run writes timeline.json: the spec with the drawn timeline, which replays it exactly.

Tests

TestSilentServerAnswersNothing, TestLoadBalancerCapsTheDraw, TestStartupDelay, TestRunMeterFaults (a late meter stands still, a glitch shows 0 W once, signed currents carry the sign), TestRandomTimelinesAreSeededAndBounded (same seed same timeline, a different seed a different one, every kind drawn over 20 seeds, house load within the bounds, events inside the run, the kinds filter). go test -race ./... passes.

Review fixes (Codex, 76e3fdf)

Finding Fix Test
Only the first session waited out startup_delay_s a connector leaving its session resets the timer TestStartupDelayEachSession
An earlier silence or offline timer ended a later fault each device counts its faults; a timer acts only for the latest TestOverlappingFaultsKeepTheLatest (fails before the fix)
Energy that flowed during meter_late was lost it is held and added when the readings move again TestLateMeterKeepsTheEnergy (fails before the fix: 0.0025 of 0.0075 kWh)

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T08:00:06.690688Z 76e3fdf PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76e3fdf605

ℹ️ 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".

Comment thread internal/ocpp/server.go
Comment on lines +2469 to +2473
if offerW > 0 {
if conn.offerSince.IsZero() {
conn.offerSince = simTime
}
if simTime.Sub(conn.offerSince) < s.startupDelay {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset the startup timer between charging sessions

With a nonzero startup_delay_s, only the first charging session reliably waits before drawing power. offerSince is cleared only when an active transaction observes a zero offer, but unplugging or stopping a transaction removes it without clearing the connector's timestamp. GenerateValues then skips that connector while idle, so plugging in again after the delay has elapsed makes the next session draw immediately. Reset the timestamp when a session ends or a new transaction starts.

Useful? React with 👍 / 👎.

Comment thread cmd/simulator/run.go Outdated
Comment on lines +551 to +553
sim.Server.SetSilent(true)
if ev.ForS > 0 {
time.AfterFunc(time.Duration(ev.ForS*float64(time.Second)), func() { sim.Server.SetSilent(false) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent expired silence timers from clearing newer faults

When two silence events overlap on the same device, the first event's timer unconditionally clears the second event's silence. For example, a 10-second silence followed five seconds later by an indefinite silence starts answering again at the original 10-second deadline, despite the documented requirement that the indefinite fault last until online. Seeded timelines can also generate overlapping timed silences. Cancel or invalidate the previous recovery timer when applying a newer fault.

Useful? React with 👍 / 👎.

Comment thread cmd/simulator/main.go
Comment on lines +4759 to +4761
if frozen {
// The meter stands still; the physics above went on.
} else if p1Buffer == nil || p1Buffer.Mode == P1ModeDirect {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve energy accumulated while meter readings are frozen

During a meter_late event with nonzero import or export, this branch skips both the register updates and energy accumulation. Each tick's energyDeltaKWh is discarded, and after recovery only subsequent ticks are added, so the cumulative meter permanently loses all energy that flowed during the delay. This turns a temporarily stale reading into incorrect energy totals for the rest of the experiment. Accumulate the energy internally while frozen and publish the accumulated totals when the meter resumes.

Useful? React with 👍 / 👎.

frahlg added a commit that referenced this pull request Oct 22, 2026
…atest; 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 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/faults-random branch from 76e3fdf to 26430d2 Compare October 22, 2026 09:00
@frahlg
frahlg force-pushed the feat/headless-run branch from b4dedb7 to fc3604d Compare October 22, 2026 09:00
frahlg and others added 3 commits October 23, 2026 07:33
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 <noreply@anthropic.com>
…atest; 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/headless-run branch from fc3604d to 72044c0 Compare October 23, 2026 05:34
@frahlg
frahlg force-pushed the feat/faults-random branch from cba7142 to 3bf737d Compare October 23, 2026 05:34
@frahlg
frahlg merged commit 5a6b847 into feat/headless-run Oct 23, 2026
@frahlg
frahlg deleted the feat/faults-random branch October 23, 2026 06:03
frahlg added a commit that referenced this pull request Oct 23, 2026
…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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 23, 2026
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant