Getting a Victron solar system into Home Assistant was easier than I expected

I have a small off-grid Victron solar system, just a fun science project. I wanted data like the current battery level in Home Assistant, so I could trigger alerts and turn things on when the battery level is high. I figured getting the data in would mean running something new, like a script polling the gear or something over Bluetooth. Venus OS, Victron’s monitoring firmware, already runs a Modbus/TCP server, though, and Home Assistant’s built-in modbus integration reads it with one config block.

The hardware is a BMV-712 shunt (the battery monitor), SmartSolar MPPT chargers, and a Phoenix inverter, all reporting to Venus OS on a Raspberry Pi Zero 2 W. Victron ships Venus OS on its Cerbo GX monitoring box, and it works fine on the Pi with a powered USB hub for the VE.Direct-to-USB cables (VE.Direct is the serial port on each Victron device).

Only one of the Pi’s two micro-USB ports carries data, the one next to the mini-HDMI, and the corner port is power-only. The Pi also has no Ethernet, and its one data port can’t both power it and run devices. A SMAYS “Micro USB OTG to Ethernet Adapter with Powered Hub” handles all of that. It plugs into the data port, back-powers the Pi, and splits the single USB controller into a 10/100 Ethernet jack and three USB-A ports. Its limit is three ports on a tight power budget, so mine runs three VE.Direct-to-USB cables plus Ethernet and drops the fourth device. I’m switching to a DuPPa USB Quadport VE.Direct instead of adding a second hub, since it puts all four isolated ports on one USB device.

Keep the Pi on that Ethernet link. Venus OS on the Pi won’t join a WiFi network with Protected Management Frames (PMF, 802.11w) enabled, even in optional mode, because it builds wpa_supplicant without PMF support. The handshake never completes, and the access point logs a password mismatch even though the password is right. Turning PMF off on the access point fixes it, but Ethernet avoids the problem.

Turn on Modbus/TCP in Venus OS under Settings > Services > Modbus/TCP. None of this needs internet, so the Pi can stay offline if you don’t mind losing VRM, Victron’s cloud dashboard. Mine has no internet access, and Home Assistant reads it over the LAN.

Every register in my configuration.yaml comes from Victron’s attributes.csv. Unit ids (the slave: key) come from unitid2di.csv. Unit id 100 reads the system service, Venus OS’s summary of the whole system, and a VE.Direct-to-USB device’s unit id depends on the USB port it enumerates on:

modbus:
  - name: victron
    type: tcp
    host: 192.0.2.21     # Venus OS on the Pi, isolated IoT network
    port: 502
    sensors:
      - name: "Battery SOC"
        slave: 100         # system service, follows the battery monitor selected in Venus OS
        address: 843       # /Dc/Battery/Soc
        input_type: holding
        unit_of_measurement: "%"
        device_class: battery
        state_class: measurement
        precision: 0
        scan_interval: 30
      - name: "Battery Voltage"
        slave: 100
        address: 840       # /Dc/Battery/Voltage
        input_type: holding
        unit_of_measurement: "V"
        device_class: voltage
        state_class: measurement
        scale: 0.1
        precision: 1
        scan_interval: 30
      - name: "Battery Power"
        slave: 100
        address: 842       # /Dc/Battery/Power, signed
        input_type: holding
        data_type: int16
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        scan_interval: 30
      - name: "Solar PV Power"
        slave: 100
        address: 850       # /Dc/Pv/Power
        input_type: holding
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        scan_interval: 30
      - name: "MPPT State"
        slave: 238         # solarcharger on ttyUSB1, per unitid2di.csv
        address: 775       # /State: 3=bulk, 4=absorption, 5=float
        input_type: holding
        scan_interval: 30

Here are the Home Assistant automations that go with this:

# automations.yaml
- alias: Victron Battery Low
  trigger:
    - platform: numeric_state
      entity_id: sensor.battery_soc
      below: 30            # fires once on the downward crossing, not every poll
  action:
    - service: notify.mobile_app_phone
      data:
        title: "Battery low"
        message: "Solar battery at {{ states('sensor.battery_soc') }}%"

- alias: Victron Battery Critical
  trigger:
    - platform: numeric_state
      entity_id: sensor.battery_soc
      below: 20
  action:
    - service: notify.mobile_app_phone
      data:
        title: "Battery critical"
        message: "Solar battery at {{ states('sensor.battery_soc') }}%"
        data:
          channel: alarm_stream_max   # bypass Do Not Disturb (Android)
          ttl: 0
          priority: high

The HACS (Home Assistant Community Store) integration hass-victron auto-discovers every register as an entity. It also pins a newer pymodbus that disables Home Assistant’s built-in modbus, and that’s more to maintain than five sensors need. Victron’s first-party Bluetooth integration reads a shunt directly into Home Assistant, and it’s the better choice for a standalone shunt with no Venus OS device. Modbus added the least here, since the Pi already runs Venus OS.