Growatt Thor with your own OCPP server: solar surplus charging, a 0 A lock and the firmware's pitfalls
The Growatt Thor is a solid 11 kW wallbox with a built-in solar mode. That mode wasn’t enough for me, though: it kept dropping charging sessions with SuspendedEVSE, and it knew nothing about my home battery or the exact readings of my grid meter. So since early 2026 my energy management system JG Energy controls the box entirely on its own – through a self-written OCPP server on a Raspberry Pi.
Here’s how it’s set up and which quirks of the Thor firmware I found along the way. Most of them aren’t in any manual.
The setup
OCPP (Open Charge Point Protocol) is really meant for charging network operators. The wallbox opens a WebSocket connection to a server and exchanges JSON messages over it. That server can just as well be a small Node.js program on your home network.
- In the Thor’s local web interface, set the OCPP server to the Pi’s address, e.g.
ws://<pi-address>:9000/. - The server must accept the subprotocol
ocpp1.6. - The Thor registers with
BootNotification, followed by a steady stream ofStatusNotification,MeterValuesandHeartbeat.
After every connect, my server sends a basic configuration once:
| Key | Value | Purpose |
|---|---|---|
MeterValueSampleInterval | 5 | readings every 5 s (10 s if rejected) |
ClockAlignedDataInterval | 0 | don’t wait for the full hour |
AuthorizeRemoteTxRequests | false | remote start without an extra Authorize step |
G_WebSocketPingInterval | 30 | keep the connection stable |
G_LCDCloseEnable | Enable / Disable | display off / on |
Two things here aren’t obvious: for the WebSocket ping you need Growatt’s own key G_WebSocketPingInterval – the standard OCPP key caused timeouts for me. And G_LCDCloseEnable = Enable means display off. I use it to switch the display on automatically when a car is plugged in, and off otherwise.
Surplus control
The Thor stays permanently in “fast” mode (G_SolarMode = 0). The actual control loop runs on the Pi:
- Every 15 seconds the server calculates the available surplus from the grid meter (Shelly Pro 3EM), the current wallbox power and the home battery.
- It subtracts a 350 W buffer – better to export a little than to charge from the grid.
- The value is smoothed and converted to amps (6–16 A).
- If the target changes by at least 1 A, the server sets
G_MaxCurrentviaChangeConfiguration.
Because the Thor can only do 6–16 A and can’t switch between single- and three-phase by itself, there are two separate power windows: 1.4–3.7 kW single-phase and 4.1–11 kW three-phase. In my case the charging cable decides which one applies.
Pausing without ending the session
When the surplus is gone, you could end the session with RemoteStopTransaction. But then every cloudy afternoon falls apart into a dozen separate sessions, and every restart needs another remote start that the car has to respond to first.
A charging profile with 0 A works better. The wallbox goes to SuspendedEVSE, the transaction stays open, and in the next sunny spell a new profile with, say, 8 A resumes charging.
One caveat: if the box sits at 0 A for too long, the Thor sometimes ends the transaction by itself and reports Preparing. At that point no profile helps any more – only a new RemoteStartTransaction. The control loop has to handle both cases.
Pitfall #1: Free Vend just starts charging
My Thor is set to Free Vend: plug in the car, charging starts, no card needed. That’s convenient – but unwanted in surplus mode. If you plug in in the evening, you don’t want the box to start pulling 11 kW from the grid.
You can recognise such a start by the idTag freevenIdTag in StartTransaction. My first approach: immediately limit every start that the server didn’t trigger itself to 0 A. That works, but the contactor still clicks briefly.
The better solution I measured on the actual system: a TxDefaultProfile with limit 0 that is already in the box before the car is plugged in prevents the contactor from closing at all. The Thor just beeps.
{
"connectorId": 1,
"csChargingProfiles": {
"chargingProfileId": 2,
"stackLevel": 1,
"chargingProfilePurpose": "TxDefaultProfile",
"chargingProfileKind": "Absolute",
"validFrom": "2026-11-11T10:00:00Z",
"validTo": "2026-11-11T12:00:00Z",
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [{ "startPeriod": 0, "limit": 0, "numberPhases": 3 }]
}
}
}
This “idle lock” has its own profile ID (2) and sits one level above the surplus control profile (ID 1, stack 0), so the two don’t interfere. When the server wants to charge, it deletes profile 2 specifically.
Pitfall #2: what happens if the server dies?
A lock that only the server can release is dangerous. If the Pi fails, the wallbox stays locked – and the car is empty in the morning.
So I tested whether the Thor honours validTo. Result: it does. Once the validity has expired, the contactor closes normally again when you plug in.
That gives you a dead man’s switch:
- The lock is valid for 2 hours at a time.
- The server renews it as soon as less than 45 minutes are left.
- If the server dies, the lock expires after 2 hours at most, and Free Vend charges again like any ordinary wallbox.
- If the server is shut down cleanly (
SIGTERM), it releases the lock immediately.
The same principle now applies to the surplus control profile. Before, a 0 A pause without an expiry could remain in the box. If the Pi then failed, Free Vend did start – but at 0 A.
Pitfall #3: ClearChargingProfile deletes more than you think
ClearChargingProfile with only a connectorId deletes all profiles – including the lock. My server calls it after every reconnect, for example, to clean up leftovers. Afterwards the lock has to be set again immediately, otherwise the box is open until the next control cycle.
The good news: the Thor can filter ClearChargingProfile by profile ID. To release just the lock, { "id": 2 } is enough.
More Thor quirks
GetCompositeScheduleis rejected (Rejected). So you can’t ask which limit is currently in effect. Whether a profile works, you can only see from the behaviour – or hear from the contactor.- Don’t write too fast. I keep at least 3 seconds between two commands and 15 seconds between two current changes. Faster sequences threw the box off track for me.
- Duplicate transactions. With cars that respond slowly, the Thor occasionally opens two transactions in quick succession for a single
RemoteStartTransaction. My server treats a second start within one minute as a duplicate; otherwise the energy of the first one gets lost. - “Car full” isn’t final. If the Thor ended the transaction because the car was full, it won’t notice when you later raise the charge limit in the car. So with enough surplus my server tries a single probe start every 20 minutes.
- Give it a moment after reconnecting. For the first 20 seconds after a reconnect my server sends no control commands.
Conclusion
With your own OCPP server you can control the Thor very precisely – more precisely than with its built-in solar mode. The effort isn’t in the protocol but in the edge cases: Free Vend, pauses, restarts and outages.
My most important rule: every lock the server sets must expire on its own. Then the worst case of an outage is an ordinary wallbox – not an empty car.
Links