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.
- System Architecture - Data flow diagrams, runbook
- ADR - Architecture Decision Records
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.
flowchart TD
Solar["Solar Panel"] -->|"DC"| MPPT["Inline MPPT\nInverter"]
MPPT -->|"AC"| Tasmota["Tasmota\nSmart Plug"]
Tasmota -->|"AC"| Grid["AC Grid"]
Battery["Battery 48V"] <-->|"DC"| Inverter["Victron\nInverter"]
Inverter <-->|"AC L1"| Grid
Grid -->|"AC L1"| Loads1["Loads L1"]
Grid -->|"AC L2"| Loads2["Loads L2\n(no inverter)"]
Tasmota -.->|"HTTP\npolling"| Script["This Script\non Cerbo GX"]
HA["Home Assistant"] -.->|"HTTP\npolling"| Script
Script -->|"D-Bus"| Inverter
Script -->|"MQTT"| Dashboard["Remote\nDashboard"]
- 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/ # Git repo root
├── main.py # Entry point — control loop, CLI, MQTT setup
├── inverter_control/ # Python package
│ ├── __init__.py
│ ├── config.py # Non-sensitive parameters (tuning, limits, flags)
│ ├── site_config.py # Sensitive config — NOT in git (see site_config.example.py)
│ ├── logic.py # SetpointCalculator, strategies, EMA, burst, D-term
│ ├── victron.py # D-Bus I/O — grid power, inverter power, setpoint write
│ ├── homeassistant.py # HA API polling with circuit breaker
│ ├── mqtt_bridge.py # MQTT subscribe/publish for external control
│ ├── console_ui.py # Terminal dashboard renderer
│ ├── console_server.py # TCP server (port 9999) for remote console
│ ├── keepalive.py # Setpoint keepalive during restart
│ ├── ui_config.py # Dashboard layout configuration
│ └── log-forwarder.py # Forwards daemontools logs to syslog
├── setup # SetupHelper-compatible installer (run by PackageManager)
├── gitHubInfo # GitHub user:branch for PackageManager auto-download
├── version # Current version (read by PackageManager)
├── deploy.sh # SSH deploy to Cerbo/Pi (dev workflow)
├── install.sh # Manual installer (legacy, prefer setup)
├── site_config.example.py # Template for site_config.py
├── tests/
│ └── test_logic.py # Unit tests for control logic
├── services/
│ └── inverter-control/
│ └── run # daemontools service runner
├── service/
│ └── log-forwarder/
│ └── run # Log forwarder service
├── LOGIC.md # Control logic documentation (EN)
├── LOGIC_RUS.md # Control logic documentation (RU)
└── release.sh # Tag, push, create GitHub release
- Copy
site_config.example.pytosite_config.py - Edit
site_config.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 site_config.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. The setup script in this repo is PackageManager-compatible and handles service creation, file placement, and restarts.
-
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:
main - Proceed → Download → Install
-
Configure secrets (from your local machine):
cp site_config.example.py site_config.py # Edit site_config.py with your HA token, Tasmota IPs, sensor names, etc. scp site_config.py root@cerbo:/data/inverter-control/ -
Done! PackageManager will auto-download updates from
mainand reinstall on Venus OS updates.
PackageManager discovers packages by scanning /data/ for directories containing both a version file and a setup script. The setup script (sourced from this repo) is executed with the INSTALL action by SetupHelper, which:
- Creates
/data/inverter-control/and copiesmain.py+inverter_control/package - Copies
site_config.pyfrom/data/setupOptions/inverter-control/or the package - Creates the daemontools service under
/service/inverter-control/ - Restarts the service
The gitHubInfo file tells PackageManager where to download from:
victron-venus:main
This means: download https://github.com/victron-venus/inverter-control/archive/main.tar.gz
For development or testing, use deploy.sh:
./deploy.sh Cerbo # 'Cerbo' is SSH host alias in ~/.ssh/configThis copies main.py, the inverter_control/ package, setup, and gitHubInfo to the device, then restarts the service.
# Copy files to Venus OS
scp -r main.py inverter_control/ setup gitHubInfo version root@cerbo:/data/inverter-control/
# SSH to Venus OS and run installer
ssh root@cerbo
cd /data/inverter-control
./setup# 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[B:+320]- Burst correction applied (sudden spike response)[D:+33]- D-term braking (prevents overshoot when approaching zero fast)[!ΔNNN]- Software fuse triggered (delta exceeded limit)
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
# Check service status
svstat /service/inverter-control
# View recent logs
tail -50 /var/log/inverter-control.log
# Check for import errors (common after refactor)
cd /data/inverter-control && python3 -c "from inverter_control.config import LOOP_INTERVAL; print('OK')"After moving modules into inverter_control/ subdirectory, the service crashes with ImportError if the package is not deployed. Symptoms: service starts, shows banner, immediately exits.
Cause: deploy.sh or PackageManager did not copy the inverter_control/ directory.
Fix:
# Check if package exists
ls /data/inverter-control/inverter_control/
# If missing, redeploy
./deploy.sh Cerbo
# Or manually
scp -r inverter_control/ root@cerbo:/data/inverter-control/
ssh root@cerbo "svc -t /service/inverter-control"PackageManager's AddStoredPackages() requires both a version file AND a setup script in /data/inverter-control/.
Check:
ls -la /data/inverter-control/version /data/inverter-control/setup
cat /data/inverter-control/gitHubInfo # should show: victron-venus:mainCommon issues:
setupfile missing → PackageManager skips the directory silentlygitHubInfopoints tolatesttag (which may be ancient) → should bemainDO_NOT_AUTO_ADDflag in/data/setupOptions/inverter-control/→ manual removal marker
Fix:
# Copy setup and gitHubInfo
scp setup gitHubInfo root@cerbo:/data/inverter-control/
ssh root@cerbo "chmod +x /data/inverter-control/setup"
# Restart PackageManager to re-scan
svc -t /service/PackageManagerVerify:
tail -20 /var/log/PackageManager/current | grep inverter
# Should show: adding inverter-control / checking inverter-controlSymptom: Console dashboard shows data on startup but never refreshes.
Cause: Service is not running, or D-Bus polling loop crashed.
svstat /service/inverter-control # Check if up
tail -20 /var/log/inverter-control.log # Check for errorsAfter 5 consecutive HA poll failures, the circuit breaker opens for 60 seconds. This is normal — HA restarts, network blips, etc.
grep "circuit breaker" /var/log/inverter-control.logIf persistent: check HA_URL and HA_TOKEN in site_config.py.
# Check VE.Bus service
dbus -y | grep vebus
# Check system data
dbus -y com.victronenergy.system / GetValue# Check MQTT broker is running
mosquitto_sub -t '$SYS/broker/uptime' -C 1
# Check inverter-control MQTT logs
grep MQTT /var/log/inverter-control.log | tail -10Python 3.6+ has a built-in secrets module. Our site_config.py relies on local import priority (current directory wins). If the working directory is wrong, Python imports the stdlib secrets instead, and all HA/EV features silently disable.
Symptoms: HA features disabled, ENABLE_HA = False in logs.
Workaround: Ensure the service cds to /data/inverter-control/ before running (the run script handles this).
Long-term fix: Rename site_config.py to site_config.py or local_config.py.
The TCP console server binds to 0.0.0.0:9999 without authentication. This provides read-only access to live inverter data. For home LAN this is fine; if the Cerbo is exposed to the internet, consider IP whitelisting or firewall rules.
- 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.