v2.2
zeroinput is a Python script for PV zero feed-in (self-consumption optimisation) on a Raspberry Pi. It reads the electricity meter via vzlogger and controls one or more battery grid-tie inverters over RS485 or USB — switching between power stages depending on load. Supported inverter types: Soyosource GTN limiter series and Victron MultiPlus (ESS via MK3-USB adapter).
zeroinput relies on vzlogger from the Volkszähler project in two ways:
- as the meter data source — vzlogger reads the electricity meter and streams the active power value (
1-0:16.7.0) to zeroinput via a FIFO - as the data logger — zeroinput writes its own values (feed-in power, battery voltage, PV power, …) back to vzlogger, which logs them alongside the other Volkszähler channels
zeroinput integrates naturally into an existing Volkszähler setup without replacing or duplicating it.
Files:
| File | Description |
|---|---|
zeroinput.py |
Main script |
input_power_staging.py |
Two-stage power distribution logic |
inverter_drivers.py |
Inverter driver abstraction (Soyosource, Victron MK3) |
charger_drivers.py |
MPPT charger drivers (eSmart3, Victron, EPever, Renogy, Morningstar) |
vebus.py |
VE.Bus MK2/MK3 protocol driver (only needed for Victron MultiPlus) |
predictor.py |
Load predictor (k-means, optional) |
webconfig.py |
HTTP configuration server (optional, requires -httpd) |
ve_aggregator.py |
VE.Direct Aggregator client (optional, only for victron_agg) |
zeroinput.conf |
Configuration (JSON) — create from zeroinput.conf.starter |
zeroinput.conf.starter |
Reference configuration showing all available device classes |
zeroinput_webconfig.html |
Web interface |
zeroinput.service |
systemd service |
zerooutput.sh |
Live status in terminal (/usr/local/bin/zerooutput.sh) |
timer.txt |
Discharge timer (optional) |
- Raspberry Pi (tested with Pi 3/4)
- One or more inverters:
- Soyosource GTN limiter (GTN-1000/1200/2000 with RS485 limiter port) — wired via USB RS485 adapter
- Victron MultiPlus / MultiPlus-II (VE.Bus, 2nd-gen microprocessor) with MK2-USB or MK3-USB adapter — ESS assistant must be configured in VEConfigure
- MPPT charge controller: eSmart3 (RS485), Victron MPPT (VE.Direct or Aggregator), EPever Tracer-AN/-BN, Renogy Rover/Elite/Adventurer, or Morningstar TriStar MPPT (all RS485/Modbus RTU)
- RS485 adapter (USB) — wiring: A+ to A+, B- to B- on all devices
- Electricity meter with extended data output (request PIN from your grid operator)
The OBIS dataset
1-0:16.7.0(net active power) must be available - Meter interface compatible with vzlogger (IR read head, Shelly 3EM/3PM, Modbus meter, …)
sudo apt install python3 python3-serial vzloggerFor Victron MultiPlus support, pyserial is already included above. No additional libraries required — vebus.py uses only the standard serial port.
sudo useradd -m -s /bin/bash vzlogger # if not already existing
sudo usermod -aG dialout vzlogger # RS485 and USB serial access
sudo cp zeroinput.py input_power_staging.py inverter_drivers.py charger_drivers.py /home/vzlogger/
sudo cp vebus.py /home/vzlogger/ # only needed for Victron MultiPlus
sudo cp predictor.py /home/vzlogger/ # optional
sudo cp webconfig.py /home/vzlogger/ # optional, only for -httpd
sudo cp ve_aggregator.py /home/vzlogger/ # optional, only for victron_agg
sudo cp zeroinput.conf zeroinput_webconfig.html zeroinput.service /home/vzlogger/
sudo cp timer.txt /home/vzlogger/ # optional
sudo chown vzlogger:vzlogger /home/vzlogger/*.py /home/vzlogger/*.conf /home/vzlogger/*.htmlTo prevent ports from changing after a reboot, assign fixed device names via udev rules.
Create /etc/udev/rules.d/99-zeroinput.rules:
# IR read head (cp210x chip)
SUBSYSTEMS=="usb-serial", DRIVERS=="cp210x", SYMLINK+="lesekopf"
# RS485 adapter (ch341 chip)
SUBSYSTEMS=="usb-serial", DRIVERS=="ch341-uart", SYMLINK+="rs485"
# MK3-USB adapter (FTDI chip)
SUBSYSTEMS=="usb-serial", DRIVERS=="ftdi_sio", SYMLINK+="mk3usb"
With multiple identical adapters, distinguish by USB port:
SUBSYSTEMS=="usb", ATTRS{devpath}=="1.1", SYMLINK+="rs485a"
SUBSYSTEMS=="usb", ATTRS{devpath}=="1.3", SYMLINK+="rs485b"
Then: sudo udevadm control --reload-rules && sudo udevadm trigger
The FIFO must exist before both vzlogger and zeroinput start. Add the following lines to /etc/systemd/system/vzlogger.service:
[Service]
ExecStartPre=/bin/mkdir -p /tmp/vz
ExecStartPre=/bin/bash -c 'test -p /tmp/vz/vzlogger.fifo || mkfifo /tmp/vz/vzlogger.fifo'Then:
sudo systemctl daemon-reload
sudo systemctl restart vzloggerTo enable the Restart buttons in the web interface, add a sudoers entry. The web interface can restart both zeroinput and vzlogger, so permit both services:
sudo sh -c 'printf "vzlogger ALL=(root) NOPASSWD: /bin/systemctl restart zeroinput\nvzlogger ALL=(root) NOPASSWD: /bin/systemctl restart vzlogger\n" > /etc/sudoers.d/zeroinput'
sudo chmod 440 /etc/sudoers.d/zeroinputzeroinput generates a template vzlogger.conf.example on startup based on the current zeroinput.conf. It already contains the mandatory 1-0:16.7.0 channel and all vz_channels with UUID placeholders.
Steps:
- Start zeroinput once —
vzlogger.conf.exampleis created in the working directory - Create a channel in the Volkszähler interface for each entry and insert its UUID
- Adjust
device(read head path) andmiddlewareURL sudo cp vzlogger.conf.example /etc/vzlogger.confsudo systemctl restart vzlogger
The 1-0:16.7.0 channel (active power) must be present.
Note: Depending on channel resolution, vzlogger may write large amounts of data. See data volume management. zeroinput uses
verbosity: 15only to receive meter values from the FIFO — surplus log entries are discarded.
Copy zeroinput.conf.starter to zeroinput.conf and adapt it to your hardware. The starter file contains all available device classes with comments. Delete the entries you do not need.
zeroinput starts even without fully configured ports, so the web interface is available immediately. Configure everything in the browser at http://<hostname>:8081/ — chargers, inverters, vz channels, alarms, and all other settings.
Important: Edit
chargersandinvertersvia the dedicated tabs in the web interface or directly in the conf file. These blocks require a restart after saving — use the restart tab.
Key settings:
| Key | Description |
|---|---|
chargers |
MPPT chargers and temperature sensors (port → device config). Structural — restart required. |
inverters |
Feed-in inverters (id → device config). Structural — restart required. |
max_input_power |
Maximum total feed-in power of all inverters combined (W). Set to 0 to disable feed-in. |
max_bat_discharge |
Maximum battery discharge power (W) |
single_inverter_threshold |
Demand level above which stage 2 activates (W) |
multi_inverter_wait |
Seconds before falling back to stage 1 |
cell_count |
Number of LiFePO4 cells in series (16 = 51.2 V, 15 = 48 V, 8 = 24/28 V). All battery voltage thresholds scale with this value. |
bat_voltage_const |
Voltage-correction factor under load (V/kW, 0 = disabled) |
free_power_export |
Free export at high battery voltage (true/false) |
heat_temp_low |
Heat protection: below this sensor temperature (°C), full power |
heat_temp_high |
Heat protection: at/above this sensor temperature (°C) the inverter is switched off, linear in between. Trigger sensor via heat_protect flag on a charger in the web interface. |
webconfig_port |
Web server port (default 8081, 0 = disabled) |
vz_channels |
Volkszähler channel mapping (editable in web interface) |
load_prediction |
Enable load predictor (true/false) |
discharge_timer |
Enable timer-based control (true/false) |
Important: Device names (
name) must be unique — zeroinput exits on startup if duplicates are found.
Example chargers block:
"chargers": {
"/dev/ttyACM0": {"name": "esmart 60", "mppt_type": "eSmart3", "pvp": 2350, "temp_display": "out"},
"/dev/ttyACM2": {"name": "VE 150/35", "mppt_type": "victron", "pvp": 1720},
"/dev/ttyACM6": {"name": "Tracer AN", "mppt_type": "epever", "pvp": 1300, "unit": 1},
"/dev/ttyACM7": {"name": "Rover Elite", "mppt_type": "renogy", "pvp": 800, "unit": 1},
"/dev/ttyACM8": {"name": "TriStar 60", "mppt_type": "morningstar", "pvp": 3000, "unit": 1}
}Charger configuration fields:
| Field | Description |
|---|---|
mppt_type |
eSmart3, victron, victron_agg, temp_sensor, epever, renogy, or morningstar |
pvp |
PV peak power (W) for the %PVp display |
unit |
Modbus slave address (epever/renogy/morningstar only, default 1) |
temp_display |
Label for the external temperature reading (eSmart3) |
Modbus charger notes: epever (Tracer-AN/-BN) runs at 115200 baud; renogy (Rover/Elite/Adventurer) and morningstar (TriStar MPPT 45/60) at 9600. The Morningstar EIA-485 port exists only on the TS-MPPT-60/M — the TS-MPPT-45 is RS-232 only and cannot share an RS485 bus. One Modbus charger per port: running several on one physical bus (multi-drop with distinct unit addresses) is not yet supported by the config — give each its own port/adapter.
Example inverters block:
"inverters": {
"base": {"name": "soyo base", "type": "soyosource", "port": "/dev/ttyACM3",
"stage": [1,2], "count": 1, "max_power": 900, "min_power": 10},
"mp2": {"name": "MultiPlus II", "type": "victron_mk3", "port": "/dev/ttyUSB0",
"stage": [2], "count": 1, "max_power": 2400, "min_power": 200}
}Inverter configuration fields:
| Field | Description |
|---|---|
type |
soyosource or victron_mk3 |
port |
Serial device path. One sender per port; use count for multiple identical units sharing a port. |
stage |
List of stages the unit runs in: [1,2] both, [1] base only, [2] stage 2 only. Empty list [] disables the unit without removing it. |
count |
Number of identical units sharing the port (they all receive one broadcast packet). |
max_power |
Maximum power per single unit (W). For GTN-2000 use 1600 W (battery mode). |
min_power |
Minimum useful power per single unit (W). Below this the unit sleeps. |
mk3_ess_sign |
1 (default) or -1 to flip feed-in direction (Victron MK3 only). |
A physically shared line (eSmart3 reading + Soyosource sending on the same wire) is expressed as two separate entries: one in chargers and one in inverters with the same port.
For Victron Aggregator (multiple MPPTs on one port):
"chargers": {
"/tmp/ttyVirtual": {
"name": "AGG", "mppt_type": "victron_agg",
"devices": {
"HQ12345ABC": {"name": "VE 150/35", "pvp": 1500, "type": "mppt"},
"TEMP-P2-S0": {"name": "Rack Temp", "type": "temp"}
}
}
}Contents of zeroinput.service:
[Unit]
Description=zeroinput PV zero feed-in control
After=syslog.target network.target ntp.service vzlogger.service
[Service]
User=vzlogger
WorkingDirectory=/home/vzlogger
ExecStartPre=/bin/mkdir -p /tmp/vz
ExecStartPre=/bin/touch /tmp/vz/output_to_vz.log
ExecStart=/usr/bin/python3 -u /home/vzlogger/zeroinput.py -web -httpd
ExecStartPost=+/bin/bash -c 'test -f /tmp/vz/vzlogger_restarted || (sleep 3 && /bin/systemctl restart vzlogger.service && touch /tmp/vz/vzlogger_restarted)'
Restart=on-failure
RestartSec=15
Nice=-1
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetsudo cp zeroinput.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable zeroinput
sudo systemctl start zeroinputCheck status:
sudo systemctl status zeroinput
journalctl -u zeroinput -fNote on output (
-v): TheExecStartline above starts zeroinput without-v. The-voption enables a verbose status output that writes several lines on every control cycle (about once per second). BecauseStandardOutput=journalis set, this output goes to the systemd journal and thus to syslog — in continuous operation this adds up to hundreds of thousands of lines per day and bloats the log. For continuous operation-vis therefore omitted; only startup, error and config-reload messages are logged. The live status is still available via the-webstatus page in the browser.-vis intended for a manual foreground start (see troubleshooting), when you want to read the output directly. If you do want the verbose output in the journal permanently, add-vand either cap the journal viajournald.conf(e.g.SystemMaxUse=) or redirect the output to a separate rotated file withStandardOutput=append:/var/log/zeroinput.logplus a logrotate rule.
| Option | Description |
|---|---|
-v |
Verbose status output to console (several lines per second). Intended for a manual foreground start; omit it in continuous systemd operation, otherwise the output bloats the journal/syslog. |
-web |
Write HTML status page (zeroinput.html) |
-httpd |
Start web configuration server (port from conf) |
-no-input |
Disable power feed-in |
-test-alarm |
Test alarm command and exit |
With -httpd, the web interface is available at http://<hostname>:8081/
Tabs:
- zeroinput.conf — Edit all hot-reloadable keys live. Structural keys (
chargers,inverters) show a restart-required notice. - chargers — Structured editor for MPPT chargers and temperature sensors (eSmart3, Victron, Aggregator with SER# table, EPever, Renogy, Morningstar). Modbus types show a
unit(slave address) field. Restart required after saving. - inverters — Structured editor for feed-in inverters (type, port, stage checkboxes, parallel units, max/min power, ESS sign). Validates for power coverage gaps before saving. Restart required.
- alarms — Per-device temperature alarms (eSmart3: int_hi/int_lo/ext_hi/ext_lo; temp sensor: ext_hi/ext_lo). Alarm commands should end with
&to avoid blocking the control loop. - vz channels — Volkszähler channel mapping as editable table
- timer.txt — Edit discharge timer rules
- restart — Restart zeroinput or vzlogger (
sudo systemctl restart <service>); one button each. Requires the matching sudoers entries (see Installation). - status — Live status page (only with
-web)
Changes to zeroinput.conf are applied automatically when saved. Keys requiring restart:
chargers,inverters(structural — drivers and reader threads are built once at startup)vzlogger_log_file,persistent_vz_file,webconfig_port
Power stages. zeroinput distributes feed-in demand across two stages. Stage 1 handles base load up to single_inverter_threshold using only stage-1 inverters. Stage 2 adds all stage-2 inverters sharing the load equally per unit — as demand grows, smaller units saturate first and the largest unit (e.g. MultiPlus) takes the remainder automatically. Feed-in is disabled via max_input_power: 0 or the timer, not by leaving stage gaps.
Coverage gap check. At startup and on saving the inverters config, zeroinput checks that the configured inverters cover the full power range from 0 to max_input_power without gaps. A gap (e.g. stage-1 Soyo ending at 900 W while stage-2 MultiPlus has min_power: 1500) triggers an unmissable warning.
Soyosource ramp rate. The inverter ramps at 400 W/s. Large load steps cause brief import or export — this is normal.
Soyosource GTN-2000. Uses the same RS485 limiter protocol as the GTN-1000/1200. Use max_power: 1600 (battery mode rating, not the higher solar-mode figure).
Victron MultiPlus. Requires an MK2-USB or MK3-USB adapter and the ESS assistant configured in VEConfigure (switch fully ON, not charger-only). No GX device required. The ESS power setpoint is written to RAM only — per-second writes are safe by design. If feed-in direction is reversed, set mk3_ess_sign: -1.
RS485 bus. Connect all devices: A+ to A+, B- to B-. Enable termination resistors on the last device if available.
VE.Direct Aggregator. Multiple Victron MPPTs can share a single serial port via an Arduino Mega 2560 or Teensy 4.1 running the VE.Direct Aggregator firmware. VE.Direct is a 3.3 V UART; connection to the Pi is via USB-UART adapter. RS485 level converters can optionally be used on either or both sides for longer cable runs. ve_aggregator.py must be in the same directory as zeroinput.py.
The load predictor (load_prediction: true, default: false) detects cyclic loads (washing machine, oven) and short high surges, stabilising feed-in to avoid over-feeding.
| Setting | Where | Description |
|---|---|---|
load_prediction |
conf | Master enable (default: false) |
min_spread_w |
conf | Minimum LOW/HIGH spread for k-means to engage (default: 150 W) |
predictor_log |
conf | Write /tmp/predictor.log (default: true) |
MAX_SPREAD_W |
predictor.py |
Maximum spread; above this the load is not treated as cyclic |
PEAK_SHORT_MAX_N |
predictor.py |
Short/long peak boundary in cycles |
LOG_FILE |
predictor.py |
Log path ('' = disabled) |
conf keys are hot-reloadable; constants in predictor.py are applied automatically on file save. Full behaviour documented in predictor_spec_en.md.
Format per entry: [device, key, vz_channel, factor]
- device — device name from
chargers,"combined"for total PV, ornullfor direct variables - key — data key (see below)
- vz_channel — UUID alias in vzlogger configuration
- factor — multiplier (e.g.
-1to invert sign)
Direct variables (device: null): power_demand, zero_shift, bat_voltage
combined: PPV, PVperc, Vbat, Ibat, Pload
eSmart3: PPV, VPV, Vbat, Ibat, Pload, int_temp, ext_temp
Victron MPPT: PPV, VPV, Vbat, Ibat, IL
Temperature sensor (AGG, type: temp): ext_temp
With discharge_timer: true and a timer.txt. Format per line:
YYYY-MM-DD HH:MM:SS <battery> <inverter> <energy_Wh>
0000-00-00 as date applies daily. Values > 100 = watts, ≤ 100 = percent of maximum.
Example:
0000-00-00 22:00:00 50 80 5000
0000-00-00 06:00:00 100 100 0
Configured in zeroinput.conf under alarms, keyed by device name. An alarm activates automatically when both threshold and command are set.
"alarms": {
"esmart 60": {
"int_hi": 60, "int_hi_cmd": "your_command &", "int_hi_interval": 300,
"ext_hi": 55, "ext_hi_cmd": "your_command &"
}
}eSmart3 supports int_hi, int_lo, ext_hi, ext_lo. Temperature sensors support ext_hi, ext_lo. Commands should end with & to avoid blocking the control loop.
Follow live output:
zerooutput.sh
# or:
tail -f /home/vzlogger/zeroinput.htmlsystemd logs:
journalctl -u zeroinput -f
journalctl -u zeroinput -n 100RS485 ports busy ([Errno 16] Device or resource busy):
An old zeroinput instance is still running.
ps aux | grep zeroinput
crontab -u vzlogger -e # remove any @reboot line
sudo rebootzeroinput does not start:
journalctl -u zeroinput -n 50
python3 /home/vzlogger/zeroinput.py -vNo FIFO: mkfifo /tmp/vz/vzlogger.fifo
No RS485 communication:
ls -la /dev/ttyACM* /dev/ttyUSB*
sudo usermod -aG dialout vzloggerMultiPlus not responding: Check that the MK3-USB adapter is connected and the ESS assistant is configured in VEConfigure. zeroinput logs "MK3 inactive" on startup if no response is received — the rest of zeroinput continues running normally.
Modbus charger (EPever / Renogy / Morningstar) shows no data: Verify the unit (Modbus slave address) matches the controller setting, the baud rate is correct (EPever 115200, Renogy/Morningstar 9600), and RS485 A/B are not swapped. The display shows PORT ERROR when a port read fails. For Morningstar, confirm the model has an EIA-485 port (TS-MPPT-60/M only).
Web server not reachable:
- Check
webconfig_portinzeroinput.conf - Start zeroinput with
-httpd - Check port conflict:
ss -tlnp | grep 8081