The fault that finally pushed me into writing my own Modbus server wasn't dramatic. It was a hole. Every evening, right around the time the boiler heater kicked in, my energy dashboard in Home Assistant would flatline for a few minutes and then snap back. Not a crash — just a gap, the kind you stop noticing until you're squinting at a graph trying to work out where 0.4 kWh went.
One connection, and everyone wants it
I run a Huawei SUN2000-8KTL-M1 with a LUNA2000 battery. The inverter talks to the outside world through a little SDongle, and the SDongle speaks Modbus TCP on port 502. The catch — and it's one a lot of Huawei owners walk straight into — is that the SDongle accepts exactly one Modbus TCP connection at a time.
And I had four things that wanted it: Home Assistant's Huawei Solar integration for the dashboard, the AC·THOR 9s that dumps PV surplus into the hot-water boiler and needs a live meter reading to modulate, evcc for the wallbox, and the FusionSolar cloud. FusionSolar is the lucky one — it rides its own channel up to Huawei's servers and never touches Modbus. The other three were elbowing each other off the single slot. Whoever connected last won; the rest got connection resets, and the dashboard got that flatline.
First fix: a transparent proxy
The obvious answer is a proxy: one process holds the single connection to the SDongle, every client talks to the proxy instead. I started with the ha-modbusproxy add-on — point it at the SDongle, have it listen on 5502, repoint Home Assistant and the AC·THOR there.
It worked. For a while. But a transparent proxy still forwards every client's read straight through to the inverter, and that surfaced a subtler problem. Modbus TCP tags each request with a transaction id, and several clients sharing one upstream connection don't coordinate those ids. Under load you can get a client receiving a response meant for someone else's request, decoding it, and quietly believing the battery sits at 7% when it's really at 70%. Rare — but wrong in the worst possible way, because it's silent.
The fix that stuck: stop talking to the inverter
So I stopped letting the clients talk to the inverter at all. Instead of forwarding reads, I poll the SDongle myself, once, on a schedule, cache every register I care about, and serve all the clients out of that cache. It's about 300 lines of asyncio Python, it runs as a systemd service on port 5502, and the inverter only ever sees one polite reader.
The reader side is a list of register batches and a loop:
Each batch is a plain function-code-3 read. I keep 50 ms between them so I'm not rushing a device that's slower than a normal Modbus meter, and the whole sweep repeats every ten seconds. That's the only conversation the SDongle ever has.
Serving the clients from cache
The server side speaks just enough Modbus to be useful. A read never touches the inverter — it's answered straight from the dict, and crucially I build the response against the calling client's own header, so the transaction-id problem simply cannot happen:
Writes are the exception. A write (function code 6) is usually battery control — telling the inverter to charge or discharge — and that has to reach real hardware, so I forward those straight to the SDongle and pass the result back. Reads are cached, writes are real. That one split is the whole design.
What it actually bought me
The part I didn't expect to enjoy this much is the decoupling. A client's poll rate is now completely independent of the inverter's. Home Assistant can ask every five seconds, the AC·THOR every second, evcc whenever it feels like it — and the SDongle still sees exactly one reader, once every ten. The flatline is gone, and the AC·THOR hasn't lost its meter value since.
It also gave me a clean place to hang the numbers I actually care about. On top of those cached registers I built a handful of template sensors: self-consumption sitting around 65.9%, autarky 76.9%, the battery turning in 97.2% round-trip efficiency. Those are exactly the figures the FusionSolar app never quite shows you in one place — and they're the subject of the next part.
The honest gotcha
A cache can go stale, and an energy dashboard that lies confidently is worse than one with an honest gap. If the SDongle drops — a firmware reboot, a flaky switch — the reader loop keeps failing and the cached values quietly age. So I log it: once the cache is older than 120 seconds a warning fires, and the retry backs off instead of hammering a device that isn't answering. Clients keep getting the last-known value, which for a power graph is the right failure mode — a held line beats a hole — but you do want to know when it's happening.
If you run Home Assistant on a VM like I do — I wrote earlier about getting it onto an Azure Linux box — dropping a 300-line Python service next to it costs almost nothing, and it's been the single most stable part of my solar setup ever since. Next part: turning those cached registers into the autarky and self-consumption numbers that actually tell you whether the battery was worth buying.


