{"id":11069,"date":"2026-09-24T04:15:03","date_gmt":"2026-09-24T04:15:03","guid":{"rendered":"https:\/\/schindlerengineering.com\/public\/?p=11069"},"modified":"2026-09-24T04:15:03","modified_gmt":"2026-09-24T04:15:03","slug":"your-victron-vrm-data-is-105-seconds-old-here-is-the-real-time-channel","status":"publish","type":"post","link":"https:\/\/schindlerengineering.com\/public\/2026\/09\/24\/your-victron-vrm-data-is-105-seconds-old-here-is-the-real-time-channel\/","title":{"rendered":"Your Victron VRM Data is 105 Seconds Old&#8230;. Here is the Real-Time Channel"},"content":{"rendered":"<p>Short version for the impatient&#8230;. <strong>The Victron VRM web API lies to you about how fresh your data is.<\/strong> Not on purpose. It just hands you the LOGGED database and never mentions there is a second channel running at 2 seconds.<\/p>\n<p>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.<\/p>\n<h2>First\u2026. MEASURE it. Do not guess.<\/h2>\n<p>I had my claude instance poll the VRM diagnostics endpoint every 10 seconds and watch the timestamp <em>on the sample itself<\/em> \u2014 not the time we fetched it. The sample carries its own age. That is the number that matters.<\/p>\n<pre>14:26:54  age  48s   V 53.72  I 16.3\n14:27:05  age  59s   V 53.72  I 16.3   &lt;- same sample\n14:27:15  age  69s   V 53.72  I 16.3   &lt;- same sample\n14:27:26  age  80s   V 53.72  I 16.3   &lt;- same sample\n14:27:37  age  91s   V 53.72  I 16.3   &lt;- same sample\n14:27:48  age 102s   V 53.72  I 16.3   &lt;- same sample\n14:27:59  age  56s   V 53.73  I 17.6   &lt;- FINALLY a new one<\/pre>\n<p>There it is. A new sample about every <strong>105 seconds<\/strong>. We were polling every 30. So we re-downloaded the <em>identical record<\/em> three or four times before it ever changed.<\/p>\n<p>Polling faster does NOTHING. It is not a rate problem. It is the wrong pipe.<\/p>\n<h2>There are TWO channels. Nobody tells you this.<\/h2>\n<p>Straight out of Victron&#8217;s own VRM manual, buried where you will never look:<\/p>\n<blockquote>\n<p>&#8220;The VRM dashboard can show real-time data, with data updates sent straight from the installation to your browser <strong>every two seconds<\/strong>, rather than pulled from the database where information is stored at the interval configured in Settings \u2192 VRM Portal \u2192 Interval.&#8221;<\/p>\n<\/blockquote>\n<ul>\n<li><strong>The database<\/strong> \u2014 what <code>\/v2\/installations\/&lt;id&gt;\/diagnostics<\/code> gives you. Logged at your GX logging interval. Mine is 1 minute, so ~105 s in practice.<\/li>\n<li><strong>The real-time feed<\/strong> \u2014 dbus-MQTT, pushed from your GX. About 2 seconds.<\/li>\n<\/ul>\n<p>VictronConnect&#8217;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 <code>DBUS-MQTT<\/code> error \u2014 which is the tell that it was using it all along.<\/p>\n<h2>The actual recipe<\/h2>\n<p>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:<\/p>\n<ul>\n<li><strong>Broker<\/strong> \u2014 <code>mqtt&lt;N&gt;.victronenergy.com:8883<\/code> where <code>N = sum(chars of your portal ID) % 128<\/code>. Mine: portal <code>c0619abc6d7f<\/code> \u2192 sums to 912 \u2192 <strong>mqtt16<\/strong>.<\/li>\n<li><strong>TLS<\/strong> \u2014 the broker presents a cert signed by Victron&#8217;s own private <em>&#8220;CCGX Certificate Authority&#8221;<\/em>. Your OS will NOT trust it. Pin their CA or you go nowhere. (It is valid until 2114. Sure.)<\/li>\n<li><strong>Auth<\/strong> \u2014 username is your VRM account <strong>email<\/strong>. Password is the literal string <code>\"Token \"<\/code> + your personal access token. <strong>Miss that space-after-Token prefix and you get rc=5 NOT AUTHORIZED<\/strong> with zero explanation. Ask me how I know.<\/li>\n<li><strong>Subscribe<\/strong> \u2014 <code>N\/&lt;portalId&gt;\/battery\/#<\/code><\/li>\n<li><strong>Keepalive<\/strong> \u2014 publish to <code>R\/&lt;portalId&gt;\/keepalive<\/code>. The GX stops publishing ~60 s after your last one.<\/li>\n<\/ul>\n<h2>S*** I got wrong. Learn from me.<\/h2>\n<p>I had this working and it STILL looked slow. Two self-inflicted wounds\u2026.<\/p>\n<p><strong>1. Do not subscribe to the whole tree.<\/strong> I used <code>N\/&lt;portalId&gt;\/#<\/code> because why not. The GX then has to serialize and shove <em>234+ topics<\/em> at you before anything settles.<\/p>\n<ul>\n<li>Whole tree \u2192 first data in <strong>71 to 149 seconds<\/strong><\/li>\n<li>Narrow (battery only) \u2192 first data in <strong>15.9 seconds<\/strong><\/li>\n<\/ul>\n<p><strong>2. Do not send a bare keepalive on repeat.<\/strong> An empty keepalive forces a FULL REPUBLISH of every topic the GX owns. I sent that every 5 seconds. For about ten minutes.<\/p>\n<p>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 \u2014 I had just DDoS&#8217;d my own gateway with the world&#8217;s most polite protocol.<\/p>\n<p>Use this instead and it streams deltas like a civilized thing:<\/p>\n<pre>{\"keepalive-options\":[\"suppress-republish\"]}<\/pre>\n<p>Send it every ~25 seconds. Narrow subscription. Done.<\/p>\n<h2>What you get<\/h2>\n<pre>+  87.5s  dt=   -    Power   1193.47   &lt;- GX wakes up\n+  89.7s  dt=  2.1s  Power   1182.72\n+  89.9s  dt=  2.4s  Current 22.0\n+ 204.0s  dt=  1.1s  Power   1080.58\n\nmin 1.1s   median 2.4s<\/pre>\n<p><strong>1 to 2 seconds. Versus 105.<\/strong> Call it fifty times fresher, from anywhere in the world, on the same token you already have.<\/p>\n<h2>Two things that will bite you<\/h2>\n<ul>\n<li><strong>The GX wakes up lazily.<\/strong> 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 \u2014 do not stare at a blank screen.<\/li>\n<li><strong>Real-time is only as good as your GX&#8217;s network.<\/strong> Logged telemetry tolerates a lousy link \u2014 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.<\/li>\n<\/ul>\n<h2>Why bother<\/h2>\n<p>Because &#8220;the battery is at 53 volts&#8221; is a different statement from &#8220;the battery WAS at 53 volts, a minute and a half ago, probably.&#8221; When you are chasing a 17 kW PV burst or watching a load step, 105 seconds is not monitoring. It is history.<\/p>\n<p>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.<\/p>\n<p style=\"letter-spacing:0.4em\"><strong>O N W A R D<\/strong><\/p>\n<hr>\n<p><em>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 \u2014 no Gradle, no dependencies. More on that another time.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Short version for the impatient&#8230;. 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 &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/schindlerengineering.com\/public\/2026\/09\/24\/your-victron-vrm-data-is-105-seconds-old-here-is-the-real-time-channel\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Your Victron VRM Data is 105 Seconds Old&#8230;. Here is the Real-Time Channel&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"nf_dc_page":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-11069","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/posts\/11069","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/comments?post=11069"}],"version-history":[{"count":1,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/posts\/11069\/revisions"}],"predecessor-version":[{"id":11070,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/posts\/11069\/revisions\/11070"}],"wp:attachment":[{"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/media?parent=11069"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/categories?post=11069"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/schindlerengineering.com\/public\/wp-json\/wp\/v2\/tags?post=11069"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}