My setup:
- 23.5 kWp PV across four roof planes (east/south/west plus a garage roof)
- 42 kWh battery on an SMA Sunny Island cluster (3× SI 8.0H-13, 18 kW continuous), Sunny Home Manager 2.0 for grid metering
- 2× KEBA P30 c-series, driven over plain Modbus TCP (not through the KEBA integration)
- Home Assistant 2026.7.4, SEM 2.0.0, in Observer Mode since installation
- Previously: a home-grown charging controller in YAML (template sensors + automations) grown over several months
Up front: the integration got me in a few hours to where my own build took months — the power-flow diagram and the cost accounting in particular. What follows is meant as a contribution, not a complaint.
- Contribution: grid-charging the house battery on an SMA Sunny Island
KNOWN_LIMITATIONS.md lists only Huawei for battery control. For force-charge there is the GenericChargeAdapter though — and it can be made to work with SMA. Here are the Modbus findings from several live test rounds on real hardware.
The wrong lead: 40149 / 40151
The obvious registers do not give proportional control:
40151(U32, write-only) —FedInSpntCom, the enable flag for accepting external power commands at all: 802 = active, 803 = inactive40149(S32, write-only) —WSpt, the active power setpoint, in watts
Live measurements (two sessions, 6+ writes in total): the watt value written has no proportional effect on charge power.
| Test round | Values written | Resulting charge power |
|---|---|---|
| Night, no PV | −500 W, −5000 W | ~16.3–17.7 kW each (20–24 kW grid import) |
| Morning, SoC 49 %, PV producing | −200, −1000, −3000, −8000 W | ~6.1–6.4 kW each |
In the second round the result was indistinguishable from the autonomous PV charging baseline measured before any command was sent (6375 W). The only demonstrable effect of 40149/40151 is the binary enable — the Sunny Island charges at whatever rate its own logic decides (plausibly a function of SoC, temperature and the battery charge curve).
An important caveat about these tests: each value was only held for 1–3 seconds. If the inverter ramps toward setpoints over minutes, the tests were far too short to show a difference. Both explanations may hold at once.
Two behaviours that affect any implementation
a) Setpoints decay on their own. Without a repeated write the device ramps power back down at a steady, near-linear 80–100 W/s and is fully back to autonomous operation after roughly a minute. This looks like a grid-code-style, ramp-limited handover rather than a bug.
Consequence: a one-shot “set and forget” write does not work — you need a periodic rewrite (I write every 30 s; an existing Gira HomeServer logic in the same house uses 15 s).
The pleasant side effect: this behaviour is a built-in failsafe. If the controller dies, the inverter returns to normal operation by itself — it never gets stuck in an unsafe state.
b) Charge power is not predictable. Across the tests, combined grid import landed anywhere between 6 and 24 kW depending on battery state. A bare enable command is therefore not safe on its own: a cutoff based on the actually measured grid power is mandatory, not a nicety. My service limit budget is 15 kW and is already policed by the wallbox logic — both control paths have to share the same budget, otherwise battery charging plus EV charging together will blow through it.
The path that now runs here: 44039 / 44041
An evcc discussion (evcc-io/evcc#10805) points to a different register pair, which is what I implemented:
44039— external active power setpoint, write-only, SMA FIX2 encoding with a ×100 scale factor, expressed in percent of rated power, not in watts-10000= −100 % (full charge power)-3000= −30 %0= hold (neither charge nor discharge)+10000= normal operation, control handed back to the inverter’s own self-consumption logic
44041— maximum active power limit, same encoding, normally left at-10000
In my case percent refers to the cluster’s 18 kW continuous rating, not to a single unit.
Encoding in Home Assistant (modbus.write_register, two 16-bit registers, big endian for both word and byte order, unit ID 3):
# Split a negative FIX2 value into two unsigned 16-bit registers
setpoint_raw: "{{ (-percent * 100) | round(0) | int }}"
setpoint_u32: "{{ setpoint_raw + 4294967296 if setpoint_raw < 0 else setpoint_raw }}"
# then:
value: "{{ [ (setpoint_u32 // 65536) | int, setpoint_u32 % 65536 ] }}"
One important detail: for “do not charge” I write +10000 (normal), not 0 (hold). Hold also blocks ordinary PV charging of the battery — you only want it while a deliberately grid-fed charge is running.
That hold is exactly what I use elsewhere, though: when the cars are deliberately charging on cheap grid power, the Sunny Island otherwise discharges into the wallboxes autonomously. In one night that moved roughly 11 kWh out of the house battery into the cars (SoC 59 % → 32 %) while grid import sat at almost zero the whole time.
Verification status — please take this seriously
I don’t want to claim more than I can back up:
| Claim | Status |
|---|---|
40149/40151 give no proportional control |
measured live, two sessions, consistent |
| Setpoints decay after ~1 min at 80–100 W/s | measured live |
| Charge power unpredictable (6–24 kW) | measured live |
-2147483648 as “value not available” |
confirmed live (see below) |
44039 percent encoding and hold/normal semantics |
implemented and running, but not yet cross-measured under load |
44041 as an effective ceiling |
not empirically confirmed, best effort |
Suggestion for SEM
The GenericChargeAdapter only needs battery_force_charge_switch (domain switch) and optionally battery_target_soc_entity. That makes SMA connectable today: an HA template switch whose actions drive the register logic above, including the periodic rewrite. SEM decides the whether and when, the site-specific logic handles the how — SEM never writes a register itself.
Two things that would help if documented:
- A note in
SUPPORTED_HARDWARE.mdthat force-charge works through the generic adapter on inverters without a brand adapter, as soon as a switch is provided. The “no discharge control” column currently reads like “no battery control at all”. - Optionally a
battery_force_charge_refresh_interval: devices like the Sunny Island need periodic rewrites or the setpoint decays. For them a singleturn_onis not enough — the repetition has to be rebuilt inside the template switch.
A safety practice I would recommend
Every live write test ran behind an abort guard: poll roughly every second, revert immediately on any anomalous reading, plus a finally: block that always reverts. Two tests produced uncontrolled power events of 17–24 kW — both self-corrected within seconds. I would not probe unknown writable registers on battery, inverter or grid-facing hardware without that.
One practical note: pymodbus 3.13 (as shipped in the HA container) renamed the slave= kwarg to device_id= — older example code raises TypeError.
A bonus note for all SMA users
SMA reports -2147483648 (“value not available”) per inverter. At night both PV inverters return this INT32 sentinel, summing to -4294967296. On my system that silently destroyed the house consumption calculation: the max(0, …) clamp returned a constant 0 all night, which in turn made the load history useless for forecasting.
Fix: clamp each inverter to >= 0 before summing. The giveaway for such sentinels is a value that is exactly a power of two, or a multiple of one.