Growatt Thor with your own OCPP server: solar surplus charging, a 0 A lock and the firmware's pitfalls

· 6 min read · Also in Deutsch

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.

After every connect, my server sends a basic configuration once:

KeyValuePurpose
MeterValueSampleInterval5readings every 5 s (10 s if rejected)
ClockAlignedDataInterval0don’t wait for the full hour
AuthorizeRemoteTxRequestsfalseremote start without an extra Authorize step
G_WebSocketPingInterval30keep the connection stable
G_LCDCloseEnableEnable / Disabledisplay 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:

  1. Every 15 seconds the server calculates the available surplus from the grid meter (Shelly Pro 3EM), the current wallbox power and the home battery.
  2. It subtracts a 350 W buffer – better to export a little than to charge from the grid.
  3. The value is smoothed and converted to amps (6–16 A).
  4. If the target changes by at least 1 A, the server sets G_MaxCurrent via ChangeConfiguration.

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 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

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