A working (in production in my system), distributed generator control system designed for off-grid solar installations with Victron energy systems. Built on a master-slave architecture using Raspberry Pi devices, it provides automated generator management with manual override capabilities, scheduling, and real-time monitoring through a modern web interface.
While designed around Victron integration, the architecture is universal—any two-wire trigger source (home automation, PLC, manual switch, or relay output) can signal GenMaster, and GenSlave can control any device with a two-wire start input (generators, pumps, HVAC, irrigation, industrial equipment). With optional Tailscale VPN and Cloudflare Tunnel support, your trigger source and controlled device can operate anywhere in the world—same building or different continents, wherever there's internet access.
- Overview
- Features
- System Architecture
- Hardware Requirements
- Tested Deployment
- Known Limitations
- Quick Start
- Configuration
- Web Interface
- API Reference
- Development & Testing
- Troubleshooting
- Security
- License
- Acknowledgments
The RPi Generator Control system automates generator management for off-grid solar installations. When your Victron Cerbo GX determines that battery levels are low and generator power is needed, it sends a signal to GenMaster, which coordinates with GenSlave to physically start the generator.
| Feature | Description |
|---|---|
| Victron Integration | Monitors GPIO signal from Cerbo GX relay for automated start/stop |
| Scheduled Runs | APScheduler-based scheduling with cron expressions |
| Manual Override | Force start/stop from web UI regardless of Victron signal |
| State Machine | Comprehensive state tracking (idle, starting, running, stopping, cooldown) |
| Feature | Description |
|---|---|
| Automation Arming | Explicit arm/disarm prevents accidental operations during startup or maintenance |
| Heartbeat System | Continuous health monitoring between GenMaster and GenSlave |
| Independent Failsafe | GenSlave automatically stops generator if communication lost for 30s |
| State Persistence | PostgreSQL database survives reboots and power failures |
| Webhook Notifications | Real-time alerts to n8n, Home Assistant, or any webhook receiver |
| Feature | Description |
|---|---|
| Real-time Dashboard | Live status updates via WebSocket |
| Dark Mode | Full dark/light theme support |
| Mobile Responsive | Works on any device |
| Container Management | Portainer integration for Docker control |
| Feature | Description |
|---|---|
| Generator Profile | Store manufacturer, model number, and serial number |
| Fuel Configuration | Configure fuel type (LPG, Natural Gas, Diesel) and expected load |
| Consumption Rates | Set fuel consumption rates at 50% and 100% load |
| Per-Run Tracking | Automatically track fuel usage for each generator run |
| Fuel History | View estimated fuel consumed in run history |
| Feature | Description |
|---|---|
| Automated Exercise | Schedule regular generator runs for maintenance |
| Configurable Frequency | Set exercise interval (weekly, bi-weekly, monthly, or custom) |
| Time Selection | Choose start time for exercise runs |
| Duration Control | Set how long each exercise run should last |
| Run Now | Manually trigger an exercise run on demand |
| Feature | Description |
|---|---|
| Run History | Complete log of all generator runs with duration, trigger, and fuel usage |
| Statistics | Daily, monthly, and all-time runtime tracking |
| System Health | CPU, memory, disk, and temperature monitoring |
| Backup/Restore | Database backup with one-click restore |
| Component | Technology | Purpose |
|---|---|---|
| GenMaster Backend | FastAPI + Python 3.11 | REST API, state machine, scheduler |
| GenMaster Frontend | Vue.js 3 + Tailwind CSS | Reactive web interface |
| Database | PostgreSQL 16 | State persistence, run history, configuration |
| Cache | Redis | Session storage, real-time data |
| Reverse Proxy | Nginx | HTTPS termination, rate limiting, security headers |
| GenSlave Backend | FastAPI (Docker) | Relay control, heartbeat responder |
| HAT | Pimoroni Automation Hat Mini | Physical relay + LCD status display |
| Component | Specification |
|---|---|
| Computer | Raspberry Pi 5 (8GB RAM) |
| Storage | 128GB NVMe SSD via PCIe adapter |
| Power | 5V 5A USB-C supply (27W recommended) |
| GPIO | Pin 11 (GPIO17) for Victron input |
| Network | WiFi or Ethernet |
| Component | Specification |
|---|---|
| Computer | Raspberry Pi Zero 2W |
| HAT | Pimoroni Automation Hat Mini |
| Storage | Quality SD card or USB SSD |
| Power | 5V 2.5A supply |
| Relay | Built-in 24V @ 2A max (GPIO16) |
| Display | Built-in 0.96" 160x80 LCD |
- 2-wire normally-open contact from Cerbo GX MK2 Relay
- Connected to GPIO17 (Pin 11) and Ground (Pin 9)
- Internal pull-up resistor enabled
This is what I've actually verified in my own off-grid setup — not just what the design supports on paper. Other hardware combinations (different Pi models, alternative relay HATs, other battery monitors) may work fine but have not been field-tested here.
| Area | Tested configuration |
|---|---|
| GenMaster | Raspberry Pi 5, 8GB, Raspberry Pi OS 64-bit |
| GenSlave | Raspberry Pi Zero 2 W |
| Relay HAT | Pimoroni Automation HAT Mini |
| Trigger source | Victron Cerbo GX MK2 relay |
| Network | LAN baseline (same-network operation works with no VPN). Tailscale optional — required for remote administration or when GenMaster and GenSlave are on different networks. Cloudflare Tunnel optional — used to expose the GenMaster web UI publicly. |
| Boot policies tested | fail_safe and preserve_state (both verified across real reboots) |
| Failure tests run | GenMaster reboot mid-operation, GenSlave reboot mid-operation, network loss between master and slave, heartbeat loss triggering GenSlave's independent failsafe, relay forced OFF on failsafe trigger |
A handful of architectural and operational realities to be aware of before deploying this in your own setup:
-
Fuel usage is estimated, not measured. The fuel-tracking number is calculated from runtime × your configured consumption-rate values (
fuel_consumption_50/fuel_consumption_100) — there is no actual fuel-flow sensor. Accuracy depends on how well your rate values match real-world performance. -
Network and security configuration is non-trivial. This project has many moving parts (nginx reverse proxy, cloudflared tunnel, Tailscale, per-route auth, IP allowlists). Read
docs/SECURITY.mdend-to-end before exposing GenMaster beyond your LAN. -
GenSlave needs privileged GPIO access. It runs as a
privilegedcontainer with host networking so the Pimoroni Automation Hat Mini can drive GPIO. That's a deliberate trade-off — if you compromise the GenSlave container, you have effective root on the Pi Zero. Seedocs/SECURITY.md#container-privilege-model. -
Tested with my specific hardware setup; other generators may need wiring/config changes. Generators differ in start-contact polarity, voltage tolerance, dry-vs-wet contacts, hold-time requirements, etc. The relay control logic itself is generator-agnostic, but the physical wiring between GenSlave's relay and your generator's start input is your responsibility to verify.
- Raspberry Pi 5 with Raspberry Pi OS (64-bit)
- Docker and Docker Compose installed
- Network connectivity between GenMaster and GenSlave
curl -fsSL https://raw.githubusercontent.com/rjsears/pizero_generator_control/main/genmaster/install.sh | sudo bash# Clone repository
git clone https://github.com/rjsears/pizero_generator_control.git
cd pizero_generator_control/genmaster
# Run interactive setup
./setup.shThe setup wizard will:
- Detect environment - Raspberry Pi, LXC container, or standard Linux
- LXC Warning - Show Proxmox configuration requirements if running in LXC
- Hardware detection - Enable mock GPIO mode if not on Raspberry Pi
- System checks - Verify memory, disk, ports, network connectivity
- Install Docker if needed (with platform-specific guidance for macOS/WSL)
- Domain validation - DNS resolution, IP matching, connectivity tests
- Configure GenSlave - IP address, API secret (prominently displayed for copying)
- Connection validation - Retries 3 times with 10-second delays if GenSlave is starting
- Configure timezone - Default America/Phoenix with host sync option
- Configure generator info - Optional manufacturer, model, fuel type, consumption rates
- Optional services - Tailscale VPN, Cloudflare Tunnel, Portainer
- Generate configs - .env, docker-compose.yml, nginx.conf
- Deploy stack - Start all containers
# Interactive setup
./setup.sh
# Show help
./setup.sh --help
# Use pre-configuration file
./setup.sh --config myconfig.conf
# Validate GenSlave connection (run after GenSlave is set up)
./setup.sh --genslave
# Update GenSlave IP/URL address
./setup.sh --genslaveip
# Show version
./setup.sh --versionTo pre-populate generator information during setup, create a gen_info.json file:
# Copy the template
cp genmaster/setup/gen_info.json.template genmaster/setup/gen_info.json
# Edit with your generator details
nano genmaster/setup/gen_info.jsonTemplate contents:
{
"manufacturer": "Generac",
"model_number": "7043",
"serial_number": "ABC123456",
"fuel_type": "lpg",
"load_expected": 50,
"fuel_consumption_50": 1.6,
"fuel_consumption_100": 2.8
}The setup wizard will detect this file and offer to use these values. Fuel type options: lpg, natural_gas, diesel. Load options: 50 or 100.
# Check container status
docker compose ps
# View logs
docker compose logs -f genmaster
# Access web interface
# https://your-domain.com or https://genmaster (via Tailscale)GenSlave runs as a Docker container on the Pi Zero 2W:
# SSH into your Pi Zero
ssh pi@genslave.local
# Download and run the setup script
curl -fsSL https://raw.githubusercontent.com/rjsears/pizero_generator_control/main/genslave/setup.sh -o setup.sh
chmod +x setup.sh
sudo ./setup.shThe GenSlave setup will:
- Install Docker - From Debian repositories (optimized for Pi Zero)
- Install Docker Compose - For container orchestration
- Prompt for API secret - Must match GenMaster's
SLAVE_API_SECRET - Configure notifications - Optional Apprise URLs for failsafe alerts
- Pull Docker image - Pre-built ARM image from Docker Hub
- Create systemd service - Auto-start container on boot
- Configure Tailscale - Optional VPN for GenMaster connectivity
- Start container - Begin listening on port 8001
# Check container status
cd /opt/genslave
docker-compose ps
# View logs
docker-compose logs -f
# Test API (requires API key)
curl -H "X-API-Key: your-api-secret" http://localhost:8001/api/healthConfiguration is managed through .env file. Key settings:
# Domain Configuration
DOMAIN=genmaster.example.com
# Application
APP_ENV=production
APP_DEBUG=false
APP_SECRET_KEY=<generated-secret>
# Database
DATABASE_USER=genmaster
DATABASE_PASSWORD=<generated-password>
DATABASE_NAME=genmaster
# GenSlave Communication
GENSLAVE_ENABLED=true
SLAVE_API_URL=http://genslave.local:8001
SLAVE_API_SECRET=<shared-secret>
# Heartbeat
HEARTBEAT_INTERVAL_SECONDS=10
HEARTBEAT_FAILURE_THRESHOLD=3
# Webhooks (Optional)
WEBHOOK_BASE_URL=https://n8n.example.com/webhook/xxx
WEBHOOK_SECRET=<webhook-secret>
# Generator Information (Optional - can be configured via UI)
GEN_INFO_MANUFACTURER=Generac
GEN_INFO_MODEL_NUMBER=7043
GEN_INFO_SERIAL_NUMBER=ABC123456
GEN_INFO_FUEL_TYPE=lpg # lpg, natural_gas, or diesel
GEN_INFO_LOAD_EXPECTED=50 # 50 or 100
GEN_INFO_FUEL_CONSUMPTION_50=1.6 # gal/hr at 50% load
GEN_INFO_FUEL_CONSUMPTION_100=2.8 # gal/hr at 100% load
# Cloudflare Tunnel
CLOUDFLARE_TUNNEL_TOKEN=<tunnel-token>
# Tailscale (Optional)
TAILSCALE_AUTHKEY=<auth-key>
TAILSCALE_HOSTNAME=genmasterEnable optional services using profiles:
# Basic stack (GenMaster, PostgreSQL, Nginx)
docker compose up -d
# With Tailscale VPN
docker compose --profile tailscale up -d
# With Cloudflare Tunnel
docker compose --profile cloudflare up -d
# With Portainer
docker compose --profile portainer up -d
# All optional services
docker compose --profile tailscale --profile cloudflare --profile portainer up -dThe dashboard provides at-a-glance status of:
- Generator State - Current status with color-coded indicator
- GenSlave Status - Online/offline with last heartbeat time
- Victron Signal - Active/inactive GPIO17 state
- System Health - CPU, memory, disk usage
- Start Generator - Manual start (requires GenSlave online)
- Stop Generator - Manual stop
- View Schedule - Manage scheduled runs
- View History - Browse run history with fuel consumption data
- Generator Information - View and edit manufacturer, model, serial number
- Fuel Configuration - Set fuel type, expected load, and consumption rates
- Exercise Schedule - Configure automated maintenance runs
- Enable/disable exercise scheduling
- Set frequency (weekly, bi-weekly, monthly, or custom days)
- Configure start time and duration
- Run exercise immediately with "Run Now" button
- Webhook Configuration - URL, secret, event toggles
- GenSlave Settings - API URL and secret
- Backup/Restore - Database backup management
- System - Timezone, logging level
# Get current status
GET /api/status
# Start generator manually
POST /api/generator/start
Content-Type: application/json
{"trigger": "manual"}
# Stop generator
POST /api/generator/stop# List schedules
GET /api/schedule
# Create schedule
POST /api/schedule
Content-Type: application/json
{
"name": "Morning Run",
"cron_expression": "0 6 * * *",
"duration_minutes": 60,
"enabled": true
}
# Delete schedule
DELETE /api/schedule/{id}# Get generator info
GET /api/generator-info
# Response
{
"manufacturer": "Generac",
"model_number": "7043",
"serial_number": "ABC123456",
"fuel_type": "lpg",
"load_expected": 50,
"fuel_consumption_50": 1.6,
"fuel_consumption_100": 2.8
}
# Update generator info (partial update)
PATCH /api/generator-info
Content-Type: application/json
{
"manufacturer": "Generac",
"fuel_type": "lpg",
"load_expected": 50
}# Get exercise schedule
GET /api/exercise
# Response
{
"enabled": true,
"frequency_days": 7,
"start_time": "10:00",
"duration_minutes": 15,
"last_exercise_date": "2026-01-12",
"next_exercise_date": "2026-01-19"
}
# Update exercise schedule
PATCH /api/exercise
Content-Type: application/json
{
"enabled": true,
"frequency_days": 14,
"start_time": "09:00",
"duration_minutes": 30
}
# Run exercise now (manual trigger)
POST /api/exercise/run-now# Health check
GET /api/health
# System metrics
GET /api/system/health
# GenSlave status
GET /api/system/slave# Health check
GET /api/health
# Relay control
GET /api/relay/state # Get current relay state
POST /api/relay/on # Turn relay ON (requires armed)
POST /api/relay/off # Turn relay OFF
# Arming
GET /api/relay/arm # Get arm status
POST /api/relay/arm # Arm automation
POST /api/relay/disarm # Disarm automation
# Heartbeat (from GenMaster)
POST /api/heartbeat # Receive heartbeat with command
# System info
GET /api/system # CPU, RAM, temperature
GET /api/failsafe # Failsafe monitor status# Get arm status
GET /api/system/arm
# Arm automation (enables all automated actions)
POST /api/system/arm
Content-Type: application/json
{"source": "api"}
# Disarm automation (blocks all automated actions)
POST /api/system/disarm
Content-Type: application/json
{"source": "api"}The arming system prevents accidental generator operations during startup, maintenance, or testing. Automation is disarmed by default and must be explicitly armed before any automated actions can occur.
- Startup Safety: Prevents race conditions when GenMaster/GenSlave boot at different times
- Maintenance Mode: Disarm before working on the generator or electrical systems
- Testing: Safely test configurations without triggering the generator
- Power Loss Recovery: Operator-configurable boot policy controls whether armed state survives across reboots (default: disarm and require re-arming)
GenMaster's boot behavior is controlled by an operator-selectable Boot Arming Policy, configured in the UI under Generator → Boot Arming Policy. The setting is stored in the database and persists across reboots. GenSlave's behavior is the same regardless of GenMaster's policy.
Boot Arming Policy options on GenMaster:
| Policy | Behavior on GenMaster boot | Use case |
|---|---|---|
fail_safe (default) |
Disarms the relay if it was armed pre-boot. Sets manual_disarm_active = True. Fires the boot_disarmed_failsafe notification. Operator must re-arm via the UI. |
Default. Required if your installation should NOT auto-resume after a power event. |
preserve_state |
Keeps the prior armed state across the reboot. The system can resume operation automatically after an outage. | Only when your installation is safe to auto-resume (proper ATS, weatherproofing, fuel/CO safety, etc.). Switching to this mode requires confirming a UI warning. |
On GenMaster boot (regardless of policy):
slave_connection_status = "unknown",missed_heartbeat_count = 0- If
generator_runningwas True, reset to False; close orphaned run withstop_reason = "error" - Apply boot arming policy:
fail_safe:slave_relay_armed = False,manual_disarm_active = True, fireboot_disarmed_failsafenotificationpreserve_state: leaveslave_relay_armedunchanged
- Reconcile with GenSlave once it's reachable
- Log
SYSTEM_BOOT_RESETevent withboot_arming_policyandrelay_disarmed_by_policyfields
On GenSlave boot (always, no policy choice):
- Relay is immediately set to OFF (hardware-default and software-enforced)
- Internal
_armed = False - Failsafe monitor starts waiting for heartbeats
- First heartbeat from GenMaster syncs both armed state and relay state — within ~1 heartbeat cycle (~10s default), GenSlave matches whatever GenMaster says
Important under fail_safe (default): The generator will NOT auto-start after a power loss, even if the Victron signal is active. An operator must explicitly re-arm the system first. The boot_disarmed_failsafe notification (configurable in Notifications → Configure → Generator Events) tells you when this happens.
Important under preserve_state: The generator may automatically restart after an outage if your battery monitor is still calling for power when the system returns. Use only when your installation can safely auto-resume.
| State | Victron Signals | Scheduled Runs | Manual Start |
|---|---|---|---|
| Disarmed | Logged but ignored | Skipped (logged) | Blocked |
| Armed | Trigger start/stop | Execute normally | Allowed |
1. Boot GenMaster and GenSlave
2. Verify GenSlave connection (should show "connected")
3. Review system status in web UI
4. Click "Arm Automation" in dashboard
5. System now responds to Victron signals and schedules
- Running Generator: Disarming does NOT stop a running generator
- Manual Stop: Use manual stop to halt generator before disarming
- Maintenance: Always disarm before physical work on generator
# Check arm status
curl https://genmaster.example.com/api/system/arm
# Response
{
"armed": false,
"armed_at": null,
"armed_by": null,
"slave_connection": "connected"
}
# Arm the system
curl -X POST https://genmaster.example.com/api/system/arm \
-H "Content-Type: application/json" \
-d '{"source": "api"}'
# Response
{
"success": true,
"armed": true,
"message": "Automation armed successfully",
"armed_at": 1736985600,
"warnings": []
}Docker images are automatically built and pushed to Docker Hub when code is merged to main:
| Image | Platforms | Trigger Path |
|---|---|---|
rjsears/genmaster:latest |
amd64, arm64 | genmaster/** |
rjsears/pizero_generator_control:genslave |
arm/v6 | genslave/** |
Manual builds can be triggered from the GitHub Actions tab with target selection.
GenMaster requires privileged container access for GPIO on Raspberry Pi 5:
# In docker-compose.yaml
genmaster:
privileged: true
user: rootThis is required because Pi 5 uses a different GPIO architecture (/dev/gpiochip0, /dev/gpiomem4) than earlier models.
GenMaster can run in LXC containers for development without GPIO hardware:
# Clone and setup
git clone https://github.com/rjsears/pizero_generator_control.git
cd pizero_generator_control/genmaster
# Run setup (auto-detects mock GPIO mode)
./setup.sh
# Or manually with docker compose
docker compose up -dWhen not running on a Raspberry Pi, GenMaster automatically:
- Detects missing GPIO hardware
- Falls back to mock GPIO mode
- Enables development API at
/api/dev/*
# Signal ON (generator wanted)
curl -X POST http://localhost:8000/api/dev/gpio/victron-signal \
-H "Content-Type: application/json" \
-d '{"active": true}'
# Signal OFF (generator not wanted)
curl -X POST http://localhost:8000/api/dev/gpio/victron-signal \
-H "Content-Type: application/json" \
-d '{"active": false}'
# Get current state
curl http://localhost:8000/api/dev/gpio/state
# Toggle signal
curl -X POST http://localhost:8000/api/dev/gpio/toggleTo test the web interface without GenSlave:
# Set in .env
GENSLAVE_ENABLED=falseThis disables heartbeat service and suppresses slave offline warnings.
| Issue | Solution |
|---|---|
| GenSlave shows offline | Run ./setup.sh --genslave to validate connection |
| GenSlave IP changed | Run ./setup.sh --genslaveip to update the URL |
| Generator won't start | Verify GenSlave is online, check for active override |
| Victron signal not detected | Check GPIO17 wiring, verify signal in /api/dev/gpio/state |
| Container won't start | Run docker compose logs <container> for details |
| Database connection failed | Check DATABASE_PASSWORD matches in both services |
| LXC Docker issues | Ensure lxc.apparmor.profile: unconfined is set in Proxmox |
| API key mismatch | Update GenSlave's .env then run docker-compose up -d --force-recreate genslave |
# Validate GenSlave connection (tests DNS, ping, port, API health)
./setup.sh --genslave
# Update GenSlave URL/IP (with optional health checks and restart)
./setup.sh --genslaveipThe --genslave option performs comprehensive validation:
- DNS resolution / IP address verification
- Ping connectivity test
- TCP port availability check
- API health endpoint test with API key authentication
- Automatic retry: If GenSlave doesn't respond, retries 3 times with 10-second delays (allows time for container restart)
# View all container logs
docker compose logs -f
# Restart specific container
docker compose restart genmaster
# Check database
docker compose exec db psql -U genmaster -d genmaster -c "SELECT * FROM system_state;"
# Test GenSlave connection manually
curl -X GET http://genslave.local:8000/api/health
# Check GPIO state (mock mode)
curl http://localhost:8000/api/dev/gpio/state| Log | Location |
|---|---|
| GenMaster | docker compose logs genmaster |
| Nginx | docker compose logs nginx |
| PostgreSQL | docker compose logs db |
| Application | /app/logs/ inside container |
- Tailscale VPN - Encrypted WireGuard mesh network
- Cloudflare Tunnel - No exposed ports, DDoS protection
- Nginx Rate Limiting - API: 30r/s, Auth: 5r/m
- JWT Authentication - Token-based API access
- HTTPS Only - No HTTP traffic accepted
- HMAC Webhooks - Signed webhook payloads
- API Secrets - Shared secrets for GenSlave communication
- Nginx Geo Module - IP-based allowlist gating the entire 443 interface (UI, API, websocket, health, Portainer). Clients outside the allowlist receive HTTP 403. Manage entries via Settings → Access Control.
- Tailscale ACLs - Tag-based access control
- Cloudflare Access - Optional additional authentication layer
| Service | Internal Port | External Access | Notes |
|---|---|---|---|
| Nginx | 443 | Yes (HTTPS only) | Main entry point |
| FastAPI | 8000 | No (internal) | Backend API |
| PostgreSQL | 5432 | No (internal) | Database |
| Redis | 6379 | No (internal) | Cache |
| Portainer | 9000 | /portainer/ path | Optional |
| GenSlave API | 8001 | Tailscale only | Pi Zero 2W |
This project is licensed under the MIT License - see the LICENSE file for details.
- Victron Energy - Cerbo GX integration
- Pimoroni - Automation Hat Mini
- FastAPI - Modern Python web framework
- Vue.js - Progressive JavaScript framework
- Tailscale - Zero-config VPN
- Cloudflare - Tunnel and security services
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- My amazing and loving family! My family puts up with all my coding and automation projects and encourages me in everything. Without them, my projects would not be possible.
- My brother James, who is a continual source of inspiration to me and others. Everyone should have a brother as awesome as mine!
Richard J. Sears
- GitHub: @rjsears
Welcome to the off-grid community


