Victron Venus OS MQTT → renewvan bus. Bridges everything a Venus OS device
(Cerbo GX, Raspberry Pi running Venus OS, …) already knows about the van's
power system onto the vendor-neutral battery, solar and charger
entities from hub's schema/. No new sensing, just a topic remap of an
existing feed.
Use this driver if the van already runs Venus OS. A van without a GX
device reads its hardware directly with a different driver; both publish the
same entities, so nothing downstream cares which one is in use. See
hub/docs/adr/0008-node-naming-and-entity-neutrality.md.
- Venus OS broker (subscribe only):
N/<portalId>/#. Venus only re-sends a value when it changes and stops publishing once its last subscriber disconnects, so the subscription is held open for the life of the process and aR/<portalId>/keepaliveis sent on connect and every 30 s so slow-changing values (a full battery's SoC) still appear. - Renewvan bus (publish only, retained):
renewvan/<entity>/<id>/*, plus driver liveness onrenewvan/venus/health(online/offlinevia MQTT LWT) — deliberately 3 segments, not 4, so it can't be mistaken for an entity keyed by a fakehealthid. - Driver info (retained):
renewvan/venus/info, one JSON message describing this driver (see Driver info). Published on every connect, immediately beforeonline, so a consumer that seesonlinealways finds info already retained. It is never cleared: it stays retained when the driver goes offline or its last will fires, so a consumer can say "offline, last seen vX". Payload shape:hub'sschema/driver-info.schema.json.
An entity's <id> is its Venus device instance (e.g. 290), the stable
per-device number Venus assigns. It is never derived from the user-editable
device name and there is no static map to maintain: a van's devices are
discovered from the feed.
Every instance also publishes two retained identity topics under its own
renewvan/<entity>/<id>/, for battery, solar and charger:
| Topic | Payload | When |
|---|---|---|
driver |
"venus" (this driver's key, the renewvan/venus/info segment) |
when the instance is first discovered, and again on every bus reconnect |
name |
the instance's label as a JSON string | Venus's CustomName, else its ProductName (<service>/<n>/CustomName / ProductName); republished when it changes and again on every bus reconnect. Cleared (empty retained payload) when Venus stops providing a usable label (null, empty or non-string). An instance Venus gives no label for publishes no name |
The dashboard uses driver to attribute an instance to this driver when
another driver (e.g. wattcycle) publishes the same entity type.
| Venus source | → renewvan topic | Notes |
|---|---|---|
system/0/Batteries[] soc/voltage/current/power |
battery/<instance>/soc_pct/voltage_v/current_a/power_w |
direct copy, signed (+charging) |
system/0/Batteries[] temperature |
battery/<instance>/temperature_c |
only when the device has a probe |
battery/<n>/TimeToGo |
battery/<n>/time_to_go_s |
Venus's null clears the topic; its ~10-day cap (863999.9375 s) is published as-is |
battery/<n>/History/TimeSinceLastFullCharge |
battery/<n>/time_since_full_charge_s |
|
system/0/SystemState/State |
battery/<id>/charge_state |
one system-wide value, fanned out to every battery id seen |
solarcharger/<n>/Yield/Power |
solar/<n>/power_w |
|
solarcharger/<n>/Dc/0/Voltage/Current |
solar/<n>/battery_voltage_v/battery_current_a |
|
solarcharger/<n>/Pv/V/Pv/I |
solar/<n>/pv_voltage_v/pv_current_a |
|
solarcharger/<n>/History/Daily/0/Yield |
solar/<n>/yield_today_wh |
kWh → Wh |
solarcharger/<n>/History/Daily/<0..N-1>/Yield, History/Overall/DaysAvailable |
solar/<n>/yield_history_wh |
one retained JSON array, newest first (index 0 = today), kWh → Wh. A Venus null slot is a null entry (no data), never 0. See below |
solarcharger/<n>/State |
solar/<n>/charge_state |
|
alternator/<n>/Dc/0/Voltage/Current/Power, Dc/In/V |
charger/<n>/voltage_v/current_a/power_w/input_voltage_v |
DC-DC charger; source = alternator |
charger/<n>/Dc/0/Voltage/Current |
charger/<n>/voltage_v/current_a |
AC charger (e.g. BlueSmart); source = ac. Venus publishes AC input power, not output, so power_w is not set |
battery/<n>/CustomName / ProductName |
battery/<n>/name |
CustomName, else ProductName; cleared when neither is usable |
solarcharger/<n>/CustomName / ProductName |
solar/<n>/name |
same |
alternator/<n>/CustomName / ProductName, charger/<n>/CustomName / ProductName |
charger/<n>/name |
same |
alternator/<n>/State, charger/<n>/State |
charger/<n>/charge_state |
246 → absorption, 247 → equalize, 248 (BatterySafe) → float; any other unmapped code (e.g. 11, power supply) → unknown |
Batteries[].state/.bmsstate/.name/.id are never read. Anything Venus
publishes that the schema has no property for is ignored. A value that goes
null clears its retained topic (an empty retained payload), so a reading
that stopped being valid does not linger on the bus.
Solar history. Venus publishes a controller's day log as one message per
day slot (History/Daily/0..30/Yield, kWh, slot 0 = today) and the slot count
in History/Overall/DaysAvailable. A fresh subscribe delivers every slot at
once and each R/<portal>/keepalive makes Venus send them all again, so the
driver folds them into one yield_history_wh array (the only stateful part of
the mapping, SolarYieldHistory). The array is published after the burst of
slot messages has been quiet for 2 s, and only when it is complete (every slot
DaysAvailable promises has been re-received since the last publish) and
different from the last one: never half-filled at startup, never half-shifted
at the day rollover (a burst that stalls waits for the rest), and not again on
every keepalive. DaysAvailable of 0 publishes nothing. A controller with fewer days logged publishes a shorter
array. Decision and reasoning: hub docs/adr/0009.
The mapping is pure (driver_venus/mapping.py, no network/MQTT client) and is
what tests/test_mapping.py exercises against fixture Venus payloads, no
broker or GX device required. Fixtures marked as captured are real payloads
from a Venus OS device with a SmartShunt, an MPPT and an Orion DC-DC charger.
Environment variables (matches hub's docker-compose.yml driver-venus:
service block):
| Variable | Default | Notes |
|---|---|---|
MQTT_HOST |
mosquitto |
Renewvan bus broker |
MQTT_PORT |
1883 |
|
MQTT_USERNAME |
(none) | |
MQTT_PASSWORD |
(none) | |
VENUS_MQTT_HOST |
(required) | The Venus OS device's own broker |
VENUS_MQTT_PORT |
1883 |
|
VENUS_PORTAL_ID |
(required) | Venus OS portal ID (N/<portalId>/…), from the device |
pip install -r requirements.txt
VENUS_MQTT_HOST=192.168.1.50 VENUS_PORTAL_ID=abc123 python -m driver_venus.mainpip install -r requirements.txt -r requirements-test.txt
pytest tests/ -vStart a release from Actions → Release → Run workflow, or
gh workflow run release.yml -f bump=minor (patch, major, or -f version=0.2.0
to set it explicitly). One run works out the next version from the latest v* tag,
tags main, creates the GitHub release and builds and publishes
ghcr.io/<owner>/driver-venus:<tag> (and :latest), which hub's deployment compose
file pins by tag (never builds from source — see
hub/docs/adr/0001-compose-services-via-pinned-images-not-git-submodules.md).
A beta from a feature branch: dispatch the workflow on that branch with prerelease
ticked. It runs the tests, publishes only its own tag (never :latest) and writes nothing
to main. The process is shared by every renewvan repo and lives in hub (hub/docs/adr/0015-one-release-process-via-reusable-workflows.md);
this repo's release.yml only says what it ships.
The release workflow passes the tag as the DRIVER_VERSION build argument, which
the Dockerfile promotes to an environment variable the driver reads (see
Driver info). Nothing else carries a version.
The driver describes itself to the dashboard with a manifest plus two runtime
values. Contract: hub's schema/driver-info.schema.json (decision record:
hub/docs/adr/0011-node-manifest-and-retained-info-topic.md).
manifest.json (repo root) is the single source of the static fields:
driver (venus, the topic key, same as renewvan/venus/health), product,
source, publishes (battery, solar, charger: one driver, three entity
types, which is how the dashboard attributes instances that do not name their driver to this driver) and
optional manufacturer / homepage. version and board_serial are
forbidden in it. The image build copies it beside the module
(driver_venus/manifest.json); the driver reads that copy first and falls back to
the repo-root file for a checkout run.
version comes from the git release tag and nowhere else. The release
workflow passes it as the DRIVER_VERSION build argument, which becomes an
environment variable the driver reads. When DRIVER_VERSION is unset or empty (a
checkout run, a local docker build) the driver reports the literal dev.
There is no version file or __version__, and the pyproject.toml version
is a fixed placeholder nothing reads.
board_serial is the Serial line of the board's /proc/cpuinfo, read once
at start. A default container can read it, so no compose or Dockerfile change
is needed. It is omitted (never faked, never unknown) when the file is
unreadable, the line is missing or empty, or the value is all zeros.
driver_venus/driver_info.py is a small helper deliberately copied into each
Python driver repo rather than shared as a package, so each driver releases
independently.
CI gate: .github/workflows/manifest.yml calls hub's reusable manifest workflow,
which validates manifest.json against driver-manifest.schema.json at a pinned hub
commit (hub-ref, the same SHA the workflow is pinned with) and fails if a manifest
containing version is accepted. Bump both together to adopt a newer contract. To check
locally:
npx --yes ajv-cli@5 validate -s path/to/hub/schema/driver-manifest.schema.json -d manifest.json