Reverse-Engineering the Platinum 3100S solar inverter's RS485 Protocol
My roof has a Platinum 3100S solar inverter (made by Diehl AKO). I recently installed a Solar-Log 500 to read its production and feed the numbers into Home Assistant — it works, but it’s a black box I don’t control, and I’d rather read the inverter directly with an ESP32 and cut out the middleman.
The catch: the Platinum speaks a proprietary serial protocol. There’s no Modbus register map, no public documentation, nothing. So this turned into a reverse-engineering project — and this post is the honest story of an attempt that, so far, has hit a wall. I’m publishing the full dataset so anyone else can have a go.
Two ports, two protocols#
The inverter exposes two serial ports, and they’re completely different animals:
- A DB9 RS-232 “service port” (true ±12 V). A developer named stendec reverse-engineered this one back in 2014 (from the SolarControl PC app, against a Platinum 3800S) and published the C source. I verified its CRC against a real capture — it’s correct. This is the documented, easy path.
- An RS-485 bus on a screw terminal, which is what the Solar-Log actually uses. Totally undocumented, and — as I’d find out — nothing like the RS-232 one.
The sensible plan is the RS-232 port. But that needs a MAX3232 level shifter, which I had to order. While waiting for it to arrive, I had a Raspberry Pi and an FT232 USB-to-RS485 dongle sitting idle… so I decided to passively sniff the Solar-Log ↔ inverter RS-485 bus and see if I could crack it.
The setup#
A Raspberry Pi 2B, the FT232 dongle wired across the bus receive-only (it
never transmits — important, you do not want to corrupt the inverter’s comms),
and a small Python script reading /dev/ttyUSB0 at 9600 baud.
I did have one thing going for me, though: the answer key was already on my
network. The Solar-Log exposes its readings over HTTP at /getjp, so every
~15 seconds I could pair each raw bus frame with the known power and voltage
values the Solar-Log had just computed from it. If a byte in the response
encoded, say, DC voltage, the correlation would jump right out. Or so I thought.
# the Solar-Log's ground-truth API
curl -X POST http://192.168.1.146/getjp -H 'Content-Type: application/json' \
-d '{"801":{"170":null}}'
# → {"801":{"170":{"101":1089,"102":1150,"103":240,"104":328,...}}}
# 101=Pac(W) 102=Pdc(W) 103=Uac(V) 104=Udc(V) ...The bus, characterised#
The Solar-Log sends the exact same 16-byte request every single cycle:
01 01 4f 03 63 a1 00 04 03 01 00 04 03 02 81 fdand the inverter replies with a 16-byte (sometimes 17-byte) response that varies. That’s the entire conversation — one request, one response, every 15 seconds. Whatever the Solar-Log displays, it derives from that one 16-byte reply.
(First sanity check, and I’m glad I did it: is the baud even right? I swept 4800 / 9600 / 19200 / 38400 and only 9600 reproduced that fixed request cleanly. A wrong baud gives you garbage, not a stable repeating frame. 9600 confirmed.)
The “breakthrough” that wasn’t#
Early on, staring at correlations between response bytes and the known voltages, I saw numbers like r = 0.94. DC voltage tracking a byte at 0.94? I thought I’d cracked it. I started writing up the win.
I was wrong, and the reason is embarrassing in hindsight. My capture ran
overnight — and at dusk and dawn the inverter is connected to the bus but
producing zero watts (Pac = Uac = Udc = 0). That off↔on step made every
voltage correlate with every response byte at 0.6–0.94. It was a confound, not
a signal. The moment I filtered to frames where the inverter was actually
producing (Pac > 100), every single correlation collapsed to noise (~0.2).
Lesson one, loudly: always check what your “signal” really correlates with. A day/night cycle will fool you every time.
Getting serious#
So I let the capture run for 28 hours — 6701 poll cycles, each paired with
its ground-truth /getjp reading. I rewrote the capture to be cleaner
(prefix-anchored on the fixed request, so noise and split frames get discarded)
and ended up with 3878 solid “inverter is producing” frames spanning Pac from
100 to 2380 W and DC voltage from 308 to 396 V. A proper dataset.
Then I threw everything at it — ten encoding hypotheses, one after another. Here’s what each was, why it was worth trying, and what actually happened.
1. Plain integer. The obvious starting point: maybe the value just sits in the bytes as a number. I took every single byte and every adjacent pair (both big- and little-endian) and correlated each against the known Pac, Pdc, Uac and Udc. Best result: about r = 0.2 — noise.
2. BCD (binary-coded decimal). Industrial meters love BCD: each nibble holds one decimal digit. I decoded every run of three and four nibbles (most-significant-digit first) at every offset and correlated. Nothing rose above the noise floor.
3. Scaled or offset (÷10 and friends). The RS-232 spec warned that scale
factors were inconsistent — voltages in 0.1 V, currents in 0.1 A. Worth checking,
except it’s already settled by a mathematical fact: Pearson correlation is
invariant under scaling and offset. If a value were stored as f(bytes) × k + c
in any units at all, the correlation would be a perfect 1.0 regardless of k. A
reading of 0.2 means it’s genuinely not a linear encoding — not a units problem.
That single insight retires hypotheses 1–3 in one stroke.
4. Timing lag. I pair each bus frame with the /getjp reading taken four
seconds later. But what if the Solar-Log reports a value from the previous cycle,
or the next? I swept lags from −3 to +3 cycles. Correlation peaked at lag zero and
fell off symmetrically on both sides — a genuinely shifted field would spike
sharply at one non-zero lag. Not a timing problem.
5. Fixed XOR keystream. Maybe the payload is XORed with a repeating key —
common in proprietary protocols, and a difficult one, because XOR is bitwise and
correlation can’t see through it at all. I tested directly: for every value and
every byte-pair I computed cipher XOR value across all frames and checked
whether it came out as one constant key. I validated the method first with a
synthetic XORed field, which scored 100% and recovered the exact key. On the real
data the best consistency was ~6% — chance. No fixed key.
6. Multiplexing by a selector byte. The header bytes take only a handful of
values (01, 08, 09 and the like), which look like page selectors. Maybe each
cycle’s response carries a different value, chosen by one of those bytes — in
which case each value would appear in only a fraction of frames and the average
correlation would smear to nothing. I binned frames by each candidate selector and
re-correlated inside every bin. Still ~0.2.
7. Round-robin multiplexing. The same idea with no selector byte at all: the inverter cycles through a sequence of data pages on each identical poll. This would explain everything I was seeing, so it was worth testing carefully. I binned frames by their position in the poll sequence (modulo N, for N from 2 to 10, using both frame index and wall-clock time) and correlated within each phase. No phase reached |r| above 0.5 for any value. Not it.
8. An energy counter. Some Solar-Log setups compute power from a cumulative energy counter rather than reading instantaneous power — which would explain why Pac refuses to correlate (it would be the derivative of something, not stored directly). For each byte-pair I computed the change between consecutive cycles and correlated that against Pac, using rank correlation so the occasional corrupted frame wouldn’t wreck it. Best result ~0.12, and no field was even strongly monotonic — a real counter marches upward at ~0.99. Not a counter.
9. Nibble-swap and bit-reversal. Two more transforms correlation can’t see through: swapping the two nibbles in each byte, and reversing the bit order (an MSB/LSB quirk). I applied each, then re-ran the whole correlation sweep. Best |r| about 0.29 — still noise.
10. The checksum. Byte 15 of the response is unmistakably a checksum — the most variable byte in the frame, changing every cycle. Cracking it would settle two things at once: validate frame integrity (so I could finally trust the bytes were clean) and confirm which bytes are payload. I tried sum8, xor8, two’s-complement sum, CRC-8 with several polynomials, and CRC-16 (CCITT, Modbus and friends), over a range of byte windows. Best match: 0.9% of frames. The checksum is non-standard and I couldn’t crack it — and an uncrackable checksum is itself a sign that the protocol is deliberately obscure.
Where it stands#
The bytes in that response do carry live data — they change with the inverter’s operating state. But it is not the power and voltage values the Solar-Log reports, in any form I could recover. My best guess is that they’re status or event codes, or raw internal quantities the Solar-Log maps through some table or formula I don’t have.
This is exactly the “from-scratch reverse-engineering job” the smart money always said RS-485 would be. I’m parking it. The documented RS-232 service port — the one stendec already cracked — is the real path forward, and the moment my MAX3232 arrives that’s what I’ll be working on. I’ll write that up when it works.
Want to try?#
I’ve published the full dataset — all 6701 frames plus the paired ground-truth readings, with a README explaining the format:
📦 platinum-3100s-rs485-capture.zip — frames + ground truth + README
The README explains how to join the two files (frames.t == solarlog.frame_t),
what each field means, and — importantly — warns you about that day/night
confound so you don’t fall into the same trap I did. If you crack the checksum,
or figure out where the watts are hiding — let me know.
Lessons#
- Beware the off/on confound. A device that reports zero at night will make every byte look meaningful. Filter to active frames first.
- Correlation is invariant to scale. If a value were stored as
f(bytes)×k+c, you’d see |r| = 1.0 regardless of units. Seeing 0.2 means it’s genuinely not a linear encoding — not a units problem. - An uncrackable checksum is a bad sign. It usually means the protocol is deliberately non-standard, and you can’t trust that your captured bytes are intact.
- Sometimes the documented path is the right one. I spent days on RS-485 anyway. The RS-232 port was already solved.
Resources#
- Download the dataset — 6701 frames + ground truth + README
- WebSolarLog — an open Solar-Log alternative; its Diehl driver is Ethernet/HTTP rather than serial, but a useful reference for which values exist
- stendec’s
RS_diehl.c— the reverse-engineered RS-232 service-port protocol (the documented path I’m ultimately taking); originally shared on the WebSolarLog Google Group in 2014