On the Road · Tech

Building an RV That Monitors Itself

Every Victron owner has the VRM app, and it is good at telling you what your system is doing. What it did not do was surface the slow-degradation modes I wanted to catch — the ones that need several sensors compared against each other. This is a running log of closing that gap.

680 AhLithium bank
920 WSolar array
2Travel uplinks
5 minHeartbeat interval

Jump to the full equipment list

How it is wired

The coach is a 2018.5 Winnebago View 24J — a diesel Class C on the Mercedes-Benz Sprinter chassis. It runs a Victron Cerbo GX as its brain: a MultiPlus inverter/charger, a 680 Ah lithium bank in two packs, a SmartSolar MPPT, a battery shunt, and an Onan generator with automatic start. Standard enough.

The part that is not standard is a small publisher script running on the Cerbo itself, pushing telemetry over MQTT into Home Assistant back at the house. That gives Home Assistant a pile of readings VRM never shows you: per-pack cell spread, the weakest cell in each pack, each pack's requested charge voltage limit, the Cerbo's own CPU load and free disk, GPS fix validity, even how many remote console sessions are open.

Home Assistant then does the math on top. That math is where most of the value turned out to be.

The fault that started all of this

The MultiPlus tripped on "low battery" every single time the air conditioner compressor kicked on. With a full bank. It was not the batteries.

Not the battery — and not really the cable either

The battery run was dropping roughly 1.5 V under the compressor's ~135 A inrush — enough to pull the voltage at the inverter's own terminals below its low-voltage cutoff while the battery itself sat happily at full charge.

The obvious reading was "undersized cable", and that is what we assumed at first. It was wrong. The run was 4 AWG rather than the 6 we thought, and more importantly the copper was only about half the problem. The rest — roughly 2.6 mΩ of it — was sitting in one corroded connection where a 4 AWG cable landed on the bus bar.

Put a number on what that means. At 125 A, 2.6 mΩ in a single lug is 0.33 V dropped and 41 watts of heat being dissipated inside one connector. And it is self-accelerating: heat grows the corrosion, corrosion raises the resistance, higher resistance makes more heat.

The rebuild replaced the run with 1/0 and cleaned the joint. Both mattered; the joint mattered more.

If your inverter trips on startup surges, meter the DC voltage at the inverter's terminals during the surge — not at the battery. Then find out where along the path the voltage is disappearing, because a bad joint and a thin cable look identical from the ends.

Read the full battery upgrade →

Now the cable is a sensor

Rather than trusting that the repair holds, Home Assistant continuously computes the resistance of each battery cable run — voltage drop divided by current, in milliohms. A corroding lug or a pitted connector now announces itself as a slowly rising number, weeks before it can strand anyone.

10.7 mΩWhile broken
2.85 mΩAfter rebuild
3.04 mΩSecond pack

It only computes above 40 A. A voltage drop measured at 5 A is mostly rounding error wearing a convincing costume.

Home Assistant DC path resistance card under load, showing branch resistance in milliohms and voltage falling across each sense point
Mid-measurement, with the air conditioning pulling 87 A. The voltage falls as you move along the path: 13.13 V at the first pack's cells, 13.00 V at the shunt, 12.84 V at the inverter's terminals. The branch figures at the top come from the cells-to-shunt span divided by that pack's own current — (13.13 − 13.00) ÷ 42.8 A gives 3.04 mΩ — not from the full cells-to-inverter drop, which spans two separately measured sections. The negative return declines to answer at all because solar was injecting 15.5 A into the middle of it.

Cell spread, with a diagnosis attached

An alert fires if either pack's cell spread holds above 50 mV for half an hour — it sits around 2 mV at rest. The alert names the weakest cell, because the pattern is what matters. If the same cell is repeatedly the outlier under comparable charge, temperature and current, that points at a persistent imbalance, a sense-path problem, or a weakening cell — run the manufacturer's balancing procedure and escalate if it persists. If the identity wanders at small spreads, that is ordinary measurement variation. A single reading tells you very little; spread under an 87 A discharge means something different from spread near the top of charge.

Home Assistant rows showing cell spread of 3 and 4 millivolts for the two packs, with the weakest cell identified in each
At rest: 4 and 3 mV against a 50 mV threshold. Note the two packs name different weakest cells — and which cell they name moves between readings. At this spread that is noise, not a finding. The value is in whether a pattern holds over weeks, which is why the alert waits half an hour before believing anything.

"Your bank never actually finished charging"

If the effective charge voltage limit drops below nominal while current is actually flowing in, Home Assistant says so and names which of the two packs is holding the bank back. Without that, you simply stop reaching a full absorption cycle. Nothing alarms, your state-of-charge calibration quietly drifts, and balancing suffers for months.

Home Assistant rows showing effective charge voltage limit at 14.05 volts, both packs requesting 14.05, and the system ceiling at 14.40
Both packs are asking for 14.05 V and the system would happily allow 14.40. That 0.35 V gap is not something throttling the bank — it is the limit the battery driver is reporting on the packs' behalf, and it is why the alert triggers below 14.0 rather than comparing against the ceiling. Compare against the ceiling and it would fire constantly and mean nothing. A managed battery lowering its own limit is normal behaviour; what is worth knowing is when it drops further than usual while charging.

The failure that hides from every alarm

A high-resistance joint in a solar string is genuinely nasty. The string self-limits to almost no output while still generating heat, so no fuse opens and no controller alarm fires. You just quietly lose a third of your array and never find out.

We found one the hard way: a burned SAE connector on the roof, charred through while still mated to its cap, one contact destroyed with green corrosion.

The likely initiator was mechanical — a branch catching the cable and pulling the connector partly out. Not far enough to disconnect anything, just far enough to reduce the contact area. That is the worst version of the fault: a fully open string stops contributing power and is therefore easy to spot, while a partly seated one keeps carrying current through a smaller contact patch, concentrates heat at that contact, and lets water into a connector with no published ingress rating. Heat and corrosion then feed each other until the housing melts.

To be clear, an open string is easier to detect, not harmless — a disconnected panel in daylight is still an energised DC source.

It is a fire risk, not a yield problem — and worth a look after any drive under low branches.

Close-up of the failed SAE solar connector, its housing melted back from the terminals with visible corrosion on the contacts
The connector itself. The housing has melted back and away from the terminals, and there is corrosion on the contacts — the pale crust on the metal. This is where a partly-seated connector ends up: less contact area, more heat, water getting in, and eventually the plastic gives up. It never blew a fuse and never raised an alarm. The only symptom was a third of the array quietly going missing.

So Home Assistant now tracks solar output health: the trailing 7-day peak output as a percentage of the trailing 30-day peak. Peaks rather than averages, because one clear hour in a week restores a peak — weather matters far less than it would to an average, though a genuinely cloudy fortnight can still pull it down. It also refuses to report at all when the rig is stored or shaded, rather than screaming that the array is dead.

What a failure looks like depends on which string goes. Ours are not equal — 400 W, 200 W and 320 W — so losing one leaves roughly 57%, 78% or 65% of nameplate respectively. String C, the one that actually failed, sits in the mid-60s. Treat this as a fault detector, not a power estimator: all three strings share one controller and do not have identical operating voltages, so they cannot all sit at their own maximum power point at once.

De-energise the array at the DC-rated string disconnects, or cover the panels, before servicing anything on the roof. Never separate PV connectors under load.

Home Assistant array health card showing output at 90.8 percent of the 30-day peak, with 7-day and 30-day peaks and a count of producing days
The sensor doing its job. This week's best hour hit 541 W against a 596 W best over the month — 90.8%, which is a healthy array. Losing one string of three would drop it to roughly 67%, and that gap is wide enough to be unambiguous. "Producing days" is the safety catch: below three, the sensor reports nothing at all rather than dividing by a month of shade and declaring the roof dead. And before anyone does the arithmetic — 596 W against 920 W of nameplate is not a general efficiency figure. The roof is flat, the strings run at different operating voltages through one controller, and the SmartSolar 100/50 is itself capped at 50 A out, which at charging voltage is roughly 700 W before anything else is considered. The array is deliberately over-panelled. That is why the useful metric is change against this rig's own history, not percentage of nameplate.

Two uplinks, and knowing which one is lying

Starlink, in detail

The dish is queried directly rather than through any app, so Home Assistant sees far more than "online": obstruction percentage, whether it is obstructed right now, outages in the last hour and the actual cause of each, the last reboot reason, thermal throttle and thermal shutdown warnings, ping latency and drop rate, and Ethernet link faults.

When service goes flaky, that is the difference between a tree, an overheating dish, and a Starlink-side outage — settled from inside the coach.

Home Assistant card showing Starlink dish state, ping latency, drop rate, throughput and Ethernet link alerts
Queried straight from the dish. A 4.76% ping drop rate against 38 ms latency is the kind of detail that turns "the internet is being weird" into something you can actually act on.
Twenty-four hour graphs of Starlink ping latency and sky obstruction percentage
A day of it. Latency sits in a 30–60 ms band with regular spikes past 100, which is simply what the service looks like — worth knowing before you go blaming your router. Underneath, sky obstruction falls from 0.18% to 0.04% across the day as the dish accumulates clear-sky time and ages out its older samples. Neither number means much on its own; the shape is the whole value.

Do not judge a cell link by one ping target

Starlink is the metered plan; a cell hotspot is the unlimited one. The router scores both every 30 seconds and prefers the unlimited link to spare the metered one.

The lesson that cost the most time: do not let one ping target be the sole proof that a cellular link is alive. On this hotspot, ICMP is deprioritized enough to produce false failures — the router concluded the link was dead, failed over to satellite, and flapped back and forth all day, burning the exact plan it was trying to protect. The honest test is an application-level probe, or several samples against more than one target.

Which link actually won is readable from the driver's seat — see the [ON] tag in the tank gauges below.

Data usage as fuel gauges

The Cerbo lets you define "virtual tanks." They are meant for holding tanks. Both of ours are spent on Starlink and cellular data usage instead, so the screen in the coach shows the month's data burn as two tank gauges sitting right next to propane.

It is the same glance-and-go read as any other tank, and it changed our behaviour more than any dashboard ever did.

There is a second trick hiding in the label. A tank's name is just a text field, so it can carry what a level gauge cannot: whichever link is currently carrying traffic gets [ON] appended to its name. The router re-picks the active uplink every 30 seconds and the label follows it. So the screen answers two questions at once — how much have I used, and which one am I on right now — without a single extra widget.

Victron GX tank screen showing LTE data at 16 percent, Starlink data at 100 percent shown in red, and propane at 38 percent
Two of these three are not tanks. Cellular data sits at 16% of its allowance, propane at 38% — and Starlink has gone red at 84.9 of 80 GB, over the cap, using the same low-level warning colour the screen would use for a nearly-empty water tank. That is precisely the point: it needs no interpretation. Look at the label on the left, too — LTE 31.2GB [ON]. That suffix is the active-link indicator, so the same screen is telling you the coach is running on cellular right now, and why: Starlink is over its cap.

Hands-free arrival, and uploads that cannot touch Starlink

When the RV comes within about 650 feet of the house, it flips its 5 GHz radio from being an access point to being a client of the home network, and demotes the cellular and satellite links to backup. Leaving flips it back. The two boundaries are deliberately different — roughly 650 feet to arrive, about 1,600 feet to leave — so it cannot sit on the edge flipping back and forth.

Two decisions there are worth spelling out.

The geofence runs on the Cerbo, not in Home Assistant. Routing it through the house would make the thing that switches the network depend on the network already being up. Home Assistant also cannot act at all while the RV is offline, which is precisely when you need this to work.

The dashcam archive is routed so it can only leave over home Wi-Fi. Not "prefers" — only. Its traffic is marked into a routing table with no fallback: if the home gateway disappears, the lookup fails into a blackhole rather than falling through to the default route. The transfer dies instead of quietly finishing over metered satellite. Dashcam footage is exactly the sort of thing that will eat an entire month's plan while nobody is watching.

There is a backstop for the ugly case, too: parked in the driveway with the house access point down, where GPS still insists you are home but the uplink is dead. After a couple of minutes it gives up and reverts to travel mode, so nobody inside is stranded without Wi-Fi.

Victron GX networking panel with an off/on switch for the RV home uplink and a slider setting the Starlink monthly cap
The switch the geofence actually operates. Arriving home does not run a script directly — it flips this, and a separate loop relays the switch to the router. One code path, so the toggle on the wall always shows the truth and a manual override sticks until the next boundary crossing. The slider underneath sets the Starlink allowance that feeds the data gauge.

Where it has been

The Cerbo's GPS posts a position to a self-hosted tracking server every ten seconds, so drives come back as real tracks rather than a start and an end. They are coloured by speed, which turns out to be the useful part — you can see where you crawled and where you were making time.

The part that took a second attempt: positions are buffered when there is no signal. Without that, every dead zone is a straight line drawn hopefully between two points, and mountain passes simply vanish. They queue on the Cerbo and upload when the link returns.

One more trap worth naming, because it cost three days. The tracking server was configured to discard any position reported at zero speed — and it returns a perfectly cheerful success code while doing it. A parked RV therefore recorded nothing at all, and looked identical to a broken tracker.

Traccar replay of an RV drive, the route coloured by speed from blue to red, with a popup showing fix time, speed and total distance
A drive played back. Blue is stopped, red is highway speed — 254 miles of it, one sample every ten seconds. The scale in the corner tops out at 73 mph, which is the honest record of a day on the road.

Alerting that survives its own outage

There is a wiring decision underneath all of this that is easy to overlook. The Cerbo, the router and the Starlink are wired ahead of the coach disconnect, so they can stay powered with everything else in the coach switched off.

That is what makes the rest of it work at all. If the telemetry chain sat behind the disconnect, shutting the coach down would take the monitoring with it — and a system that goes quiet when you switch it off is indistinguishable from one that has failed. It also means the rig keeps reporting through storage and between trips, which is precisely when nobody is looking at it.

The obvious objection is the standing draw. In practice solar covers it — with the coach off, the array carries the day's usage, so the rig can sit for weeks reporting on itself without walking the bank down. That only holds while the panels see sky, which is the one thing to think about if it is ever stored under cover.

The hardest part of monitoring anything remote is that a broken system cannot report that it is broken. Traditional alerting fails in exactly the case you care about most.

So the RV sends a heartbeat every five minutes, and the alert fires when the heartbeats stop. Separately, monitors reach into the RV over the VPN from the other direction. Read together, they tell you which half failed: heartbeat dead but the RV still answering means the sender broke; both dark means the link did.

The same care goes into the small stuff. Sensors expire after five minutes, so "no data" is the offline signal — no separate ping needed. Offline detection keys on both the Cerbo's uptime and the battery state of charge, so a battery driver fault does not get misreported as the whole RV being gone. And if a sensor is ever renamed and simply does not exist, the system reports unknown and fires nothing at all, rather than waking someone at 2 a.m. for something no amount of driving to the RV can fix.

The Cerbo watches itself as well. It rebooted once from a hardware watchdog after sustained CPU saturation, so now there is a warning as load climbs — along with alerts for a full root filesystem, unexpected restarts, and a nudge when someone leaves a remote console tab open, since each one burns about a quarter of a CPU core indefinitely.

An alert should tell you what to go do, not just that a number moved.

Uptime Kuma dashboard filtered to the RV monitors, showing the dead-man heartbeat, router and Traccar position age, with an event log of a heartbeat failure and recovery
The watchdog, caught in the act. Read the event log on the right from the bottom up: the heartbeat was arriving normally, then "No heartbeat in the time window" — the alert firing — and 99 seconds later it is back, reporting "cerbo up 68668s, load 2.37, rv-mqtt ok". The heartbeat carries diagnostics rather than just a pulse, so the recovery message tells you what state the RV came back in. "Traccar position age — last position 8 s old" is the monitor that answers what none of the others can: the RV is reachable, but is it actually producing? The red row is an unrelated service; this is a working dashboard, not a staged one.

Everything in the rig

The full parts list, because "what are you running?" is the question that always comes up first.

The coach2018.5 Winnebago View 24J

ItemDetail
CoachWinnebago View, 24J floorplan, 2018.5
ChassisMercedes-Benz Sprinter — diesel Class C
Why it mattersA short Sprinter roof is why the array is three small strings rather than two big ones, and the diesel genset shares the coach's own fuel tank

Power conversionVictron, all of it

ComponentModelRole
Inverter / chargerMultiPlus 12/3000/120-50, 120 V3000 VA inverter (2400 W continuous at 25 °C), 120 A charger, 50 A transfer switch
System controllerCerbo GXRuns Venus OS and Node-RED; the brain of the whole system
Solar controllerSmartSolar MPPT 100/50Single controller for all three strings
Battery monitorBMV-712 SmartShunt in the battery negative; coulomb-counted bank state of charge, which needs periodic synchronisation
DC-DC chargerOrion-Tr Smart 12/12-30Charges the house bank from the alternator while driving

Battery bank680 Ah · 8.7 kWh at 12 V

ComponentDetailSpec
Lithium packs2 × SFK 340ELR LiFePO4, paralleled, each BMS on its own USB link680 Ah
Pack cabling1/0 throughout, roughly 3 ft per pack run
DisconnectsAnderson SB175, one per pack175 A
USB hubCoolGear industrial 7-port — carries both packs and the Bluetooth adapter; the GPS keeps the Cerbo's second port
Chassis battery maintainerTRIK-L-START, diode-modified for lithium — keeps the chassis starting battery topped up from the house bank
Measured path resistanceTypical per-pack cells-to-shunt path after the 2026 rebuild. A per-branch fit, so shared segments count twice over — not the parallel equivalent of the bank~3 mΩ

That last row used to be 7.4 mΩ. Fixing it is the story in the section above, and the full upgrade — fitment, charge profile, and what to know before paralleling two packs — is written up in The Lithium Upgrade.

Solar920 W in three 2-panel strings

StringPanelsNameplate
A2 × Newpowa Pro 200 W 9BB400 W
B2 × Zamp Obsidian 100 W200 W
C2 × Newpowa 160 W320 W

All three strings are natively MC4, adapted to the roof's Zamp SAE cap. Worth knowing: Zamp SAE is reverse polarity to the usual SAE convention — never assume an adapter is wired the way you expect. String C is the one whose connector burned through.

Generator and shore

ComponentDetail
GeneratorCummins Onan QD3200 Quiet Diesel, with automatic start and stop driven by the Cerbo on state-of-charge thresholds. It draws from the coach's own diesel tank, so autostart spends driving range — and the pickup stops around a quarter tank so it cannot strand you
Run detectionWired into a Cerbo digital input, so "is it actually running" is measured rather than assumed
Transfer switchSecond digital input; separate AC current limits for shore and generator
Victron GX generator panel reading Stopping, auto-started on state of charge condition, with autostart enabled
Caught mid-cycle: "Auto-started · SOC condition". Nobody pressed anything — the bank fell to its threshold, the generator ran, and it is shutting down again on its own. The inverter beside it is in Sustain, holding the bank rather than cycling it.
Victron GX generator settings showing a 15 minute minimum run time and quiet hours enabled from 21:00 to 08:00
The settings behind it. Quiet hours run 21:00 to 08:00 — the part your campground neighbours care about, and the reason automatic starting is tolerable at all in a shared site. The 15-minute minimum run time matters for the generator's own sake: short starts are how you wet-stack a diesel.

Connectivity

ComponentModelRole
RouterMikroTik hAP ac (RB962UiGS-5HacT2HnT)Router, switch, dual-band access point, firewall, and the VPN endpoint
SatelliteStarlink MiniRoam plan, 100 GB/month — the metered link
CellularUSB-tethered Android phoneUnlimited, and therefore the preferred link
Cell boosterWilson RV cellular boosterAmplifies the signal reaching the phone — the difference between a usable cellular link and none at all in marginal sites
Home uplink5 GHz radio in client modeJoins the house network automatically on arrival
TunnelWireGuard, site to siteRemote administration rides this; no inbound management services are published to the internet

Cameras and sensors

ComponentModelNotes
DashcamBlackVue DR900S-1CHLive view in Home Assistant; footage archives over home Wi-Fi only
Cameras ×2Blink Outdoor, with sync moduleBattery-powered, on the RV's own 2.4 GHz network. Integrated into Home Assistant, so exterior motion and snapshots land alongside the rest of the RV's telemetry rather than in a separate app
Temperature ×4RuuviTag, BluetoothInside, outside, freezer, and inverter bay
GPSu-blox 7 USB receiverFeeds the map, the tracking server, and the arrive-home geofence
PropaneCerbo built-in tank inputOne of four inputs; three are still free

Not here yet: fresh, grey and black tank levels. Still the factory panel only.

Software

ComponentRuns whereJob
Venus OSCerbo GXVictron's own platform — everything else is built on top of it
Node-REDCerbo GXRelays the arrive/leave decision to the router
Custom collectorsCerbo GXTelemetry publisher, Starlink dish reader, GPS poster, geofence, heartbeat
Home AssistantAt the houseEvery derived sensor, dashboard and alert on this page
TraccarAt the houseDrive tracks, with offline buffering through dead zones
Uptime KumaAt the houseThe heartbeat watchdog and the reach-in monitors

The honest gaps

Water tanks

Fresh, grey and black still exist only on the factory monitor panel. There is no history, no alert, and nothing that could catch a black tank filling faster than usual — the early signature of a leaking toilet valve. Three of the Cerbo's four tank inputs are free and propane already proves the path works, so the open question is what kind of sender the coach actually uses. That is the next project.

The GPS receiver also dropped its fix for three days without anyone noticing, which is its own small lesson: it now publishes an explicit "no fix" rather than simply going quiet, so the difference between a dead receiver and a dead RV is visible at a glance.

More to come as things break and get fixed. Questions, or want detail on any of it? Get in touch.

Built with Claude Code.