EV charger simulator: a HEART-embedded system you can run and watch#
A demonstration only. It simulates a home EV charger, the cars that use it, the grid it hangs on and its HEART. It is not a charger, controls no hardware, and must never be used to run, test or certify one.
One Python file, evChargerSimulator.py, with only the standard library. Run it, register it on HEART Hub with the code it prints, and watch its health on your own private page while it lives a simulated life: cars arriving in the evening and leaving in the morning, each with its own battery and charging curve; warm afternoons and cold nights; grid voltage dips and power cuts; a home router that restarts; and, staged one after another, a frozen energy meter, an overheating connector, an unstable then lost supply, a lost backend link, a residual current device (RCD) trip, a contactor that will not close and a contactor that welds.
It simulates a home charger, not a public site: one tethered connector, 32 A single-phase (about 7.4 kW), one household. That makes the privacy question sharp: when a car charges shows when people are at home.
This HEART changes nothing. Charging current, start and stop, and every protection stay with the charger's certified controller and its operator's backend. Real chargers talk to their operator's backend over OCPP (the Open Charge Point Protocol); this simulator only names that link, it does not implement OCPP, and neither does HEART.
The case study behind it, step by step for a real product, is guides/EV-Charger.md.
On a real device: EV Charger, for real shows the hardware we assume, where the HEART runs, the code to change, how to build and install it, the factory steps and what the owner sees. For illustration only.
| File | What it is |
|---|---|
evChargerSimulator.py | The simulator: cars and grid, charger, HEART, local HEART API, HEART Hub reporting |
test_e2e.py | End-to-end test against a monitor site on your own computer |
README.md | This file |
1. Download and run#
You need Python 3.8 or newer. Nothing to install.
python3 evChargerSimulator.py --scenario all
It starts at once and prints three things:
HEART Hub: register this simulated EV charger
1. Sign in at https://heartcortex.ai/login with your email (a 6-digit code is sent to you).
2. Open https://heartcortex.ai/register?id=5be2c90f13a7d468
3. Enter the claim code Q4N8T-ZK2MA-7RWXB-HD3PE
4. Watch it at https://heartcortex.ai/device/5be2c90f13a7d468 (private to you until you share it)
19:02:11 HEART API on http://127.0.0.1:43306/heart/v1/pulse (owner key for private views: ...)
19:02:26 car 12% · charging 32.0 A 7.42 kW · grid 231 V · connector 20.8 °C · RCD ok · contactor ok · link up · HEART healthy
Every 15 seconds it prints one line of what the simulated charger measures. When something goes wrong, its HEART says so:
19:16:01 HEART critical system.main-function-lost: The residual current device has tripped: charging is stopped.
Earth-leakage protection tripped (a residual current in the car, the cable or the wiring). The charger is
safe and will stay off. A qualified person must inspect the car and cable and reset it at the charger.
HEART never resets it, and nothing can do so remotely.
19:18:02 charger: RCD reset by a person at the charger, after checking the car and cable
19:18:02 HEART resolved system.main-function-lost: the residual current device has tripped: charging is stopped
Stop it with Ctrl-C. The device ID, claim code and key are kept in ev-charger-monitor.json (readable only by you), so the same simulated charger comes back next time. Delete that file to make a new device.
2. Register it and watch it on HEART Hub#
- Sign in. Open
https://heartcortex.ai/loginand enter your email address. A 6-digit code arrives; type it in. There are no passwords. - Register. Open the
register?id=...link the simulator printed, and enter the claim code. The claim code is the proof that you hold the charger: it is shown only on the simulator's console (on a real charger: a label inside the cover, or the installer's app), and the site holds only its hash. - Watch. Your device's page,
https://heartcortex.ai/device/<ID>, shows its status (healthy, degraded or failing) and its open and recent alerts, each with the plain-language meaning of its fault code. All your devices are athttps://heartcortex.ai/devices. - Share, if you want. The page is private until you make a share link or make it public. Sharing it with your installer is the usual reason.
- Silence. Stop the simulator, or wait for the staged network link loss: after three missed Pulse intervals (90 seconds) the page shows a Flatline, and you get one email. The site checks for silent devices every 5 minutes, so the email can take a few minutes more. When it reports again, you get one more.
Until you register it, the site stores nothing: the simulator's reports are answered "not registered yet" and dropped. After registering, it can take up to five minutes for the first report to arrive, or less as soon as something changes.
What the site sees. Only the public Pulse and public alerts, and it keeps only the status and timing and each alert's fault code, severity and times. Never a current, a kilowatt-hour, a connector state, a charging session or a temperature. An alert's timing can still hint at use (an RCD trips while a car is charging): see Step 8 of the case study.
3. The staged faults#
With --scenario, a demo car (82 kWh, 12 % full) plugs in at once and charges through the run, and each fault starts at a set time, lasts a while, and is then "fixed by the electrician", so you can watch an alert open, escalate and resolve. Times are in simulated seconds: at --speed 1 they are real seconds.
| Scenario | Starts | Lasts | What you see |
|---|---|---|---|
meter | 1 min | 2 min 20 s | sensors.stuck (error) after a minute: 32 A flows but the energy register does not move |
hot | 3 min 40 s | 4 min | performance.too-hot (warning): a worn connector pin; the connector passes 70 °C and the charger derates itself from 32 A to 16 A |
grid | 8 min | 2 min | power.supply-unstable (warning) for a minute (dips below 207 V, swells above 253 V; the charger pauses itself below 195 V), then power.supply-lost (error) for a minute |
link | 10 min 20 s | 3 min | connectivity.link-lost (warning). The network is down, so no reports: the page shows a Flatline at --speed 1, then it recovers |
rcd | 13 min 40 s | 2 min | system.main-function-lost (critical): the RCD trips on an earth leak; the charger stays off until a person resets it at the charger |
contactor | 16 min | 1 min 40 s | system.main-function-lost (critical) and actuators.not-responding (error): the contactor's coil is burnt, it will not close |
weld | 18 min | 2 min | The demo car leaves and the contactor stays closed: welded. system.main-function-lost (critical) and actuators.not-responding (error); the charger locks itself out until the contactor is replaced |
all | All of the above |
Without --scenario, cars come on random timers (most in the evening, few at night) and faults still happen now and then, as in life, but rarely: a voltage dip every 1 to 4 hours, the home router every 6 to 24 hours, a power cut every few days, an earth leak every 4 to 12 days. A hot afternoon warms the connector but does not reach derating.
4. Options#
| Option | Default | Meaning |
|---|---|---|
--scenario | none | meter, hot, grid, link, rcd, contactor, weld (comma-separated) or all |
--speed | 1 | Simulated seconds per real second. --speed 5 runs the staged faults in about 4 minutes (too fast for Flatline emails, which need real minutes) |
--monitor-url | https://heartcortex.ai | Any HEART Hub site, including your own. http only for 127.0.0.1 |
--no-monitor | Report to no site: local only | |
--state-file | ev-charger-monitor.json | Where the device ID, claim code and key are kept |
--port | 43306 | The local HEART API (falls back to 7685; 0 for none) |
--seed | random | Repeat the same simulated life |
--duration | until Ctrl-C | Stop after this many real seconds |
5. The local HEART API#
The simulator also serves the HEART API on your own computer, as a real charger would on its local network:
curl http://127.0.0.1:43306/.well-known/heart-card.json # the Card: what it is
curl http://127.0.0.1:43306/heart/v1/pulse # public health
curl http://127.0.0.1:43306/heart/v1/arrhythmias # public alerts: codes only
curl http://127.0.0.1:43306/heart/v1/properties # public values: in service or not, only
KEY=... # the owner key it printed
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43306/heart/v1/properties # all values
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43306/heart/v1/arrhythmias # alerts with explanations
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43306/heart/v1/sessions # private charging sessions
curl -X PUT -H "Authorization: Bearer $KEY" http://127.0.0.1:43306/heart/v1/properties # 403: HEART changes nothing
The sessions view lists each charging session's start, end, energy and peak current, and nothing else: no card ID, no vehicle ID, no account. Every change (PUT, POST, PATCH, DELETE, on any path, with or without the owner key) is refused with 403 capability-not-allowed.
Every message it serves is a valid HEART message: test_e2e.py checks each one against the official schemas.
6. How the code is built#
World ──► Charger ──(read only)──► Heart ──► local HEART API (127.0.0.1)
cars, grid, the certified watches └► HEART Hub (outbound only)
weather, controller and its
network protections
| Part | In the file | In a real product |
|---|---|---|
Car | One visiting car: battery size, state of charge, target, on-board charger limit, a charging curve that tapers above 80 % | A real car, talking to the charger over the control pilot (IEC 61851) or ISO 15118 |
World | Cars arriving on random timers (mostly evenings), outdoor temperature by hour, grid voltage with dips and outages, the home network, rare earth leaks | The real world |
Charger | The controller: offers current (never above the cable's 32 A), closes and opens the contactor, counts energy, derates above 70 °C, stops at 90 °C, pauses outside 195 to 260 V, latches on an RCD trip or a weld | The charger and its certified firmware. Never part of HEART |
Heart | Reads the charger's values; RULES turn them into alerts with fault codes; its own check that the meter moves while current flows; escalate() tells a person; serves only what each caller may see | The code you add, on the charger's communication board |
monitor() | Hello, then reports to HEART Hub; follows the site's nextSec; stops for a wrong key; says hello again if forgotten; quiet while the network link is down | The same, unchanged |
handler() | The HEART API: Card, Pulse, alerts, properties, sessions; every change refused | The same, behind TLS on the local network |
Nothing in Heart can write to Charger: there is no setter, no command and no capability. That is the design, not an omission.
7. What code to add to make a real product HEART-embedded#
For a real charger, HEART runs on the communication board (the part that already runs the network link and OCPP), never in the safety controller that drives the contactor and the RCD: see Step 1 of the case study. Pick a starting point, then add four things.
Starting points (all in this project)
| Your communication board | Start from |
|---|---|
| Embedded Linux (most wallboxes with Wi-Fi, Ethernet or LTE) | This file's Heart, monitor() and handler(); or the Python template heart-templates/templates/python; or the Rust Core heartd (heart-core-rs) for HEART Full |
| A microcontroller with Wi-Fi (ESP32 class) | The ESP32 Full starter heart-templates/templates/esp32-full (its signal table, main-job check and Monitor reporting) |
| A Node.js OCPP client already on the board | The JavaScript template heart-templates/templates/javascript-node, beside it |
| A bare microcontroller with no OS | heart-templates/templates/c-baremetal |
The four things you add
- Read the charger. Replace
World,CarandChargerwith a reader for the values your controller already publishes to the communication board (the same ones it sends to the backend as OCPP status and meter values). It must be read only: HEART gets values, never a way to send anything back.
``python class ChargerReadings: """CHANGE: fill these from your controller's existing status messages. Read only.""" def refresh(self): s = your_controller.latest_status() # your code self.state, self.current, self.voltage = s.connector_state, s.current_a, s.voltage_v self.meter, self.temp = s.energy_register_kwh, s.connector_temp_c self.rcd_tripped, self.derating = s.rcd_tripped, s.thermal_derating self.command_closed, self.closed = s.contactor_command, s.contactor_mirror self.welded = s.weld_detected self.link_down = not s.backend_connected ``
- Your rules and thresholds. Edit
RULESandEXPLAINwith the limits from your charger's own specification (its derating curve, its voltage window, its meter's resolution). Keep the standard fault codes (Part 3), so every installer tool and monitor site reads them the same way. - Your escalation route. Change
escalate()to notify the owner's app and the installer, and to show the fault on the charger's own LED ring or display, so a person standing at the charger knows, with no internet needed. - Your privacy table. Go through
properties()and give every value its level (Step 3 of the case study). Anything that shows when a car charges isprivate; maintenance values areinternal; only status, in-service and fault codes arepublic. Keep card IDs, vehicle IDs and account numbers out of HEART entirely.
Then keep monitor() as it is, store the device ID, claim code and key in the board's secure storage, and print the claim code and a QR code of https://<your monitor site>/register?id=<ID> on a label inside the cover. Serve the API over TLS (or keep it on loopback), run the HEART-Rating client and Part 9's tests, and claim HEART Basic, Autonomy Level A1.
8. Test it#
From the project's top folder, with Node 20+ and Python 3 with pip install --user "jsonschema[format]>=4.18" referencing:
python3 evChargerSimulator/test_e2e.py
It starts a HEART Hub site on this computer (port 8806), runs the simulator with every staged fault at 5x speed (local API on port 43306), signs in, registers the device through the site's own form, and checks for about four minutes that every message is valid, public views hold only public data, charging sessions need the owner key and hold no card or vehicle ID, every change is refused with 403 capability-not-allowed even with the owner key, every staged fault reaches the owner's page with its fault code, no measurement reaches the site, and the page stays private.
Licence: Apache-2.0. Part of HEART, the open standard at heartcortex.ai.