Short version for the impatient…. The Victron VRM web API lies to you about how fresh your data is. Not on purpose. It just hands you the LOGGED database and never mentions there is a second channel running at 2 seconds.
I found this the annoying way. My phone app felt slow and crusty. VictronConnect on the same phone was showing me numbers that moved. Mine were not moving. GRRR.
First…. MEASURE it. Do not guess.
I had my claude instance poll the VRM diagnostics endpoint every 10 seconds and watch the timestamp on the sample itself — not the time we fetched it. The sample carries its own age. That is the number that matters.
14:26:54 age 48s V 53.72 I 16.3
14:27:05 age 59s V 53.72 I 16.3 <- same sample
14:27:15 age 69s V 53.72 I 16.3 <- same sample
14:27:26 age 80s V 53.72 I 16.3 <- same sample
14:27:37 age 91s V 53.72 I 16.3 <- same sample
14:27:48 age 102s V 53.72 I 16.3 <- same sample
14:27:59 age 56s V 53.73 I 17.6 <- FINALLY a new one
There it is. A new sample about every 105 seconds. We were polling every 30. So we re-downloaded the identical record three or four times before it ever changed.
Polling faster does NOTHING. It is not a rate problem. It is the wrong pipe.
There are TWO channels. Nobody tells you this.
Straight out of Victron’s own VRM manual, buried where you will never look:
“The VRM dashboard can show real-time data, with data updates sent straight from the installation to your browser every two seconds, rather than pulled from the database where information is stored at the interval configured in Settings → VRM Portal → Interval.”
- The database — what
/v2/installations/<id>/diagnostics gives you. Logged at your GX logging interval. Mine is 1 minute, so ~105 s in practice.
- The real-time feed — dbus-MQTT, pushed from your GX. About 2 seconds.
VictronConnect’s remote view uses the second one. That is the whole reason it felt alive and mine felt dead. When that channel breaks, VictronConnect pops a DBUS-MQTT error — which is the tell that it was using it all along.
The actual recipe
No library. Hand-rolled MQTT 3.1.1 over TLS. Here is everything you need, because I could not find it written down in one place anywhere:
- Broker —
mqtt<N>.victronenergy.com:8883 where N = sum(chars of your portal ID) % 128. Mine: portal c0619abc6d7f → sums to 912 → mqtt16.
- TLS — the broker presents a cert signed by Victron’s own private “CCGX Certificate Authority”. Your OS will NOT trust it. Pin their CA or you go nowhere. (It is valid until 2114. Sure.)
- Auth — username is your VRM account email. Password is the literal string
"Token " + your personal access token. Miss that space-after-Token prefix and you get rc=5 NOT AUTHORIZED with zero explanation. Ask me how I know.
- Subscribe —
N/<portalId>/battery/#
- Keepalive — publish to
R/<portalId>/keepalive. The GX stops publishing ~60 s after your last one.
S*** I got wrong. Learn from me.
I had this working and it STILL looked slow. Two self-inflicted wounds….
1. Do not subscribe to the whole tree. I used N/<portalId>/# because why not. The GX then has to serialize and shove 234+ topics at you before anything settles.
- Whole tree → first data in 71 to 149 seconds
- Narrow (battery only) → first data in 15.9 seconds
2. Do not send a bare keepalive on repeat. An empty keepalive forces a FULL REPUBLISH of every topic the GX owns. I sent that every 5 seconds. For about ten minutes.
Result: I loaded my own Cerbo into the ground. VictronConnect went to a crawl, took almost a minute to open, and my SmartShunt stopped showing up in the device list entirely. I thought I had broken the shunt. I had not — I had just DDoS’d my own gateway with the world’s most polite protocol.
Use this instead and it streams deltas like a civilized thing:
{"keepalive-options":["suppress-republish"]}
Send it every ~25 seconds. Narrow subscription. Done.
What you get
+ 87.5s dt= - Power 1193.47 <- GX wakes up
+ 89.7s dt= 2.1s Power 1182.72
+ 89.9s dt= 2.4s Current 22.0
+ 204.0s dt= 1.1s Power 1080.58
min 1.1s median 2.4s
1 to 2 seconds. Versus 105. Call it fifty times fresher, from anywhere in the world, on the same token you already have.
Two things that will bite you
- The GX wakes up lazily. It connects to the broker on demand. Expect 15 to 90 seconds before the first value lands. Paint your last known value immediately and swap to live when it arrives — do not stare at a blank screen.
- Real-time is only as good as your GX’s network. Logged telemetry tolerates a lousy link — small, periodic, retried. A persistent stream does not. My Cerbo lives in an aluminum battery box (yes, a Faraday cage, I KNOW) and the stream stalls whenever the WiFi gets marginal. Ethernet is the real fix.
Why bother
Because “the battery is at 53 volts” is a different statement from “the battery WAS at 53 volts, a minute and a half ago, probably.” When you are chasing a 17 kW PV burst or watching a load step, 105 seconds is not monitoring. It is history.
My app now shows the age of the data on every screen, counting up by the second. If the feed stalls, the number climbs and goes amber. It cannot pretend to be live when it is not. That, more than the speed, is the part I actually wanted.
O N W A R D
Hardware: 2x EG4 12000XP in parallel, Victron Cerbo GX MK2, 600A SmartShunt, off-grid in Scotts Valley CA. The app is a hand-rolled native Android APK — no Gradle, no dependencies. More on that another time.