Skip to main content
  1. Posts/

Reverse-Engineering the Platinum 3100S solar inverter's RS485 Protocol

· David Steeman · Electronics, Raspberry Pi, Linux

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 fd

and 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