Grid-zero feed-in controller for Victron systems with split-phase compensation.
Disclaimer: Most grid-zero goals can be achieved using Victron's built-in ESS Optimized (without BatteryLife) mode. This project exists for specific edge cases requiring custom logic (split-phase compensation, EV charger exclusion, multiple solar sources, etc.). This code was developed for a particular setup and is unlikely to work as a drop-in solution — treat it as a learning resource or starting point for your own implementation.
This repository did not start as a polished Python package. Roughly three years ago it began as the smallest thing that could work: a single shell pipeline glued together with mosquitto_sub, a few arithmetic hacks, and a helper script. No repository structure, no D-Bus abstraction, no Home Assistant — just “read a number from MQTT, clamp it, hand it to the inverter.”
The original one-liner looked conceptually like this (host, topic, and credentials are redacted; *** stands in for a password or token):
# Proof-of-concept from ~2023 — do not run as-is; values and paths were local.
mosquitto_sub -L "mqtt://mqtt:***@10.10.10.10/home/power_main" | while read -r line; do
# va = “current” setpoint, s = “main” sensor, b = computed next setpoint
b=$(( va - s/3 + 2 ))
[ "$b" -gt 2000 ] && b=2000
[ "$b" -le -2000 ] && b=-2000
[ "$b" -le 0 ] && b=0
[ "$s" -eq 0 ] && b="${va}"
echo -n "$(date) => current:${va} main:${s} new:${b} "
va="${b}"
~/inverter.py "${va}"
doneWhat it was trying to do, in plain language:
- Subscribe to a Home Assistant (or broker) topic that published something like “main” grid or power telemetry (
power_main). - Derive a new inverter setpoint
bfrom the difference between a remembered valuevaand the live readings(thes/3+2term was a crude proportional tweak). - Clamp the result into a safe band (±2000 W in this sketch) and avoid sending meaningless negatives in some cases.
- Delegate the actual Victron write to a tiny
~/inverter.pyhelper — the predecessor of today’s D-Bus layer.
For the curious, the same idea in one dense line (again: redacted broker URL; line breaks only for readability — the spirit was “pipe MQTT into a tiny state machine, then inverter.py”):
mosquitto_sub -L "mqtt://mqtt:***@10.10.10.10/home/power_main" \
| while read -r _; do
b=$((va-s/3+2)); [ $b -gt 2000 ]&&b=2000; [ $b -le -2000 ]&&b=-2000
[ $b -le 0 ]&&b=0; [ $s -eq 0 ]&&b="${va}"
echo -n "$(date) => current:$va main:$s new:$b "; va="${b}"; ~/inverter.py ${va}
doneThat pipeline was enough to prove the idea on a bench or a single meter. It was also fragile: no persistence across reboots, no split-phase awareness, no EV or laundry logic, and no story for MPPT + Tasmota + multiple battery chains. Everything you see now — structured config, victron.py, MQTT bridge, optional dashboard, monitoring hooks — grew out of replacing that one-liner piece by piece while keeping the same core goal: keep the grid where we want it without sacrificing the weird parts of a real house.
If you are browsing this repo for inspiration, that history is intentional: start simple, measure, then automate. The current code is the same instinct with years of production bruises folded in.
This Python application controls a Victron inverter to maintain zero grid feed-in/consumption while supporting various operating modes. It's designed for split-phase (120/240V) systems where L2 loads need to be compensated by L1 export.
[Solar] → [MPPT] → [Battery] ← → [Inverter] ← → [Grid L1]
↓
[Tasmota PV] → [AC Grid] ←------------|
|
[Loads L1] ←------|
[Loads L2] ←------ Grid L2 (no inverter)
- Grid-Zero Control: Maintains net zero power at the utility meter
- Split-Phase Compensation: Exports on L1 to offset L2 consumption
- Multiple Operating Modes:
- Normal: Automatic grid-zero targeting
- Only Charging: Use solar only, don't discharge battery
- No Feed: Only use Tasmota PV, no battery
- House Support: Tasmota PV minus 300W
- Charge Battery: Force battery charging
- Do Not Supply Charger: EV charges from grid only
- Minimize Charging: Auto-control dump loads to consume excess solar
- Home Assistant Integration: Sensor data and switch control
- Fast Control Loop: 3 updates per second via D-Bus
inverter_control/
├── config.py # Non-sensitive parameters
├── secrets.py # Sensitive config (not in git)
├── secrets.example.py # Template for secrets.py
├── main.py # Main control loop and console output
├── victron.py # D-Bus interface for Victron devices
├── homeassistant.py # HA API with caching and fallback
├── deploy.sh # Deploy to Venus OS
├── install.sh # Install on Venus OS
├── LOGIC.md # Control logic documentation (EN)
└── README.md
- Copy
secrets.example.pytosecrets.py - Edit
secrets.pywith your actual values:
# Home Assistant connection
HA_URL = "http://YOUR_HA_IP:8123"
HA_TOKEN = "your_long_lived_access_token"
# Victron Portal ID (from VRM)
PORTAL_ID = "your_portal_id"
# Tasmota device IPs
TASMOTA_IPS = ['192.168.x.x', '192.168.x.x']
# HA Sensors, VUE sensors, booleans, etc.
# See secrets.example.py for full template- Edit
config.pyfor non-sensitive parameters:
# Power limits (protect outlet from overheating)
POWER_LIMIT_MAX = 2250 # Max feed-in (W)
POWER_LIMIT_MIN = -2300 # Max export (W)
# Control loop timing
LOOP_INTERVAL = 0.33 # 3 times per secondFeatures can be enabled/disabled in config.py. They auto-disable if HA_TOKEN is not configured:
ENABLE_EV = True # EV charging monitoring (car SoC, charger power)
ENABLE_WATER = True # Water level, pump and valve control
ENABLE_HA_LOADS = True # Home Assistant loads monitoring (Vue sensors)
ENABLE_HA = True # Home Assistant integration entirelyWhen disabled:
- Console output omits the corresponding sections
- No HA API calls are made for disabled features
This allows running the inverter control standalone without Home Assistant.
The easiest way to install is via SetupHelper PackageManager:
-
Install SetupHelper (if not already installed):
wget -qO - https://github.com/kwindrem/SetupHelper/archive/latest.tar.gz | tar -xzf - -C /data mv /data/SetupHelper-latest /data/SetupHelper /data/SetupHelper/setup -
Add package via GUI:
- Settings → PackageManager → Inactive packages → new
- Package name:
inverter-control - GitHub user:
victron-venus - Branch/tag:
latest - Proceed → Download → Install
-
Copy secrets.py (run from your local machine):
# Create secrets.py from example cp secrets.example.py secrets.py # Edit secrets.py with your HA token, sensor names, etc. # Copy to Cerbo ./postinstall.sh
-
Done! The package will automatically reinstall after Venus OS updates.
cd inverter_control
./deploy.sh Cerbo # 'Cerbo' is SSH host alias# Copy files to Venus OS
scp -r inverter_control root@cerbo:/data/
# SSH to Venus OS
ssh root@cerbo
# Run installer
cd /data/inverter_control
./install.sh# Check status
svstat /service/inverter-control
# Restart
svc -t /service/inverter-control
# Stop / Start
svc -d /service/inverter-control
svc -u /service/inverter-control
# View logs
tail -f /var/log/inverter-control/current | tai64nlocal# Set specific setpoint and exit
python3 main.py 1500
# Dry run (don't send commands)
python3 main.py --dry-run- Targets zero grid power
- Automatically adjusts based on consumption and solar
- During daytime low electricity rates
- Don't discharge battery
- Use MPPT solar only, minus offset
- Only use Tasmota PV inverters
- Don't discharge main battery
- Setpoint = Tasmota PV power
- Tasmota PV minus 300W
- Supports house loads partially
- Force setpoint to 2200W
- Maximum battery charging
- EV charges from grid only
- Battery doesn't supply EV charger
- Grid calculation excludes EV consumption
- Automatically turns on/off dump loads
- Uses excess solar instead of grid export
HH:MM:SS[flags]>setpoint(prev) g:total(L1+L2)net tt(L1+L2) tt:home [State]battW,soc%,b1%,b2% solar loads water car
Example:
14:23:45[OC:850-60]>-790(0) g:45(23+22)50 567(300+267) tt:580 [External control]-150W,85%,82%,83% 890(120+130+640) 45f 150l 42cm 78%
Flags:
[~]- Grid near zero, keeping stable[EV:XXX]- EV power excluded from grid calculation[OC:XXX-60]- Only charging mode (MPPT minus offset)[NF]- No feed mode[HS]- House support mode[NoEV]- EV charger exclusion limit applied[CHG]- Charge battery mode[MC+/-]- Minimize charging load changes
For accurate grid-zero control, you need real-time power measurement at the grid entry point. Here are the options:
Any Shelly device with external CT (current transformer) clamp input works well:
- Shelly Pro 3EM - 3-phase, Ethernet + WiFi, local MQTT
- Shelly EM - Single phase, WiFi, local MQTT
- Low latency (~100ms), fully local, no cloud dependency
Vue energy monitors can work but have significant limitations:
| Version | Pros | Cons |
|---|---|---|
| Vue 2 | Affordable, easy setup | Cloud-only by default (us-east-2 = high latency), 2.4GHz WiFi only |
| Vue 3 | Has Ethernet port | ESPHome reflash may not work with Ethernet, falls back to WiFi |
Vue with ESPHome: You can reflash Vue 2/3 with ESPHome for local MQTT, eliminating cloud latency. However:
- Vue 2: No Ethernet, 2.4GHz WiFi can introduce jitter
- Vue 3: Ethernet support in ESPHome is experimental, may not work
Official Victron solutions like VM-3P75CT (3-phase CT meter):
- Pros: Native D-Bus integration, no additional software needed
- Cons:
- Expensive (~$300+)
- Requires Ethernet cable to electrical panel (often in garage)
- Reports instantaneous values which can make control loop less stable than averaged readings
For most setups, Shelly with CT clamp offers the best balance:
- Local MQTT with sub-100ms latency
- Ethernet option (Pro models) for reliability
- Affordable (~$50-80)
- Easy integration with this controller
If already using Vue with cloud, it still works but expect:
- 500-2000ms latency from us-east-2 cloud
- Occasional missed readings
- Less responsive grid-zero tracking
cat /var/log/inverter-control/current | tai64nlocal | tail -50# Check VE.Bus service
dbus -y | grep vebus
# Check system data
dbus -y com.victronenergy.system / GetValue# Test from Venus OS
curl -H "Authorization: Bearer YOUR_HA_TOKEN" \
http://YOUR_HA_IP:8123/api/states/sensor.your_sensor- Python 3.x (included in Venus OS)
- requests (for HA API)
- D-Bus (for Victron communication)
This project is part of the Victron Venus OS integration suite:
| Project | Description |
|---|---|
| inverter-control (this) | Advanced ESS external control system with grid-zero targeting |
| inverter-dashboard | Real-time web dashboard (Python/FastAPI) via MQTT |
| inverter-dashboard-go | High-performance Go rewrite of the web dashboard |
| inverter-desktop | Native desktop application (Rust/Tauri) for system monitoring |
| dbus-mqtt-battery | MQTT to D-Bus bridge for JBD BMS battery integration |
| dbus-tasmota-pv | Tasmota smart plug integration as a PV inverter on D-Bus |
| esphome-jbd-bms-mqtt | ESP32 Bluetooth monitor for JBD BMS batteries |
| inverter-monitoring | TIG (Telegraf, InfluxDB, Grafana) monitoring stack |
| terraform-github-victron | Infrastructure as Code for the GitHub organization |
Use commit.sh for automated commit and PR creation:
# Create commit message in commit.txt
echo "Add new feature X" > commit.txt
echo "" >> commit.txt
echo "Detailed description of changes" >> commit.txt
# Run commit script
./commit.shThe script will:
- Create feature branch if on main
- Commit changes
- Push branch
- Create PR with auto-merge label
- Enable auto-merge after CI checks pass
For maintainers, PRs created with auto-merge label automatically merge after:
- All GitHub Actions checks pass
- Status checks: CI, Python Security Scan
Configure branch protection rules in GitHub settings to require these checks.
Created by @4alvit
MIT License
- Fork the repository
- Create a feature branch (
git checkout -b feature-name) - Commit your changes
- Push to the branch (
git push origin feature-name) - Create a Pull Request
For issues specific to:
- D-Bus errors: Verify VE.Bus service and Venus OS version
- Home Assistant: Test token and sensor availability
- Grid metering: Check Shelly/Vue connection and MQTT latency
- Operating modes: Review mode-specific logic implementation
- This project: Open an issue in this repository
Note: This is a community project and is not affiliated with Victron Energy.