Project Overview
This project automates real-time monitoring and reporting of Delta G4 inverters. It reads sensor data over RS485, logs metrics to a MySQL database, and posts energy and power figures to PVOutput.org.
Purpose
Provide a headless, cron-ready solution to:
- Continuously poll Delta inverters for status and production data
- Persist readings in MySQL for local dashboards or analysis
- Push per-inverter and aggregated system metrics to PVOutput.org
Supported Hardware
- Delta G4 series inverters
- RS485 serial interface (e.g. USB↔RS485 adapters)
- Linux or Raspberry Pi environments
Key Features
- Serial-level command/response handling (frame building, CRC validation)
- Parsing of firmware, date/time, voltage, current, power and energy readings
- Configurable multi-inverter support
- MySQL logging for historical data
- Automated posting to PVOutput.org with API key and system ID
- Error retry and timeout management
Data Flow
- Serial Communication
Inverter.py opens the RS485 port, builds request frames, validates and parses responses. - Data Collection
DeltaPVOutput.py reads configuration, instantiates Inverter clients for each address, and retrieves raw readings. - Aggregation & Logging
Script aggregates total power/energy, inserts per-inverter and system rows into MySQL. - PVOutput.org Upload
DeltaPVOutput.py formats data for the PVOutput API and issues HTTP POSTs for each inverter and the system total. - Scheduling
Unix cron triggers DeltaPVOutput.py at defined intervals for continuous monitoring.
Installation & Quick Start
Get your Delta G4 inverter talking to PVOutput.org in minutes. This guide covers hardware hookup, system setup, configuration and a first upload.
1. Requirements
• Host: Linux/Raspberry Pi (Raspbian Wheezy or later)
• Python 2.7+ or 3.x
• RS485-to-USB adapter (e.g. FTDI)
• PVOutput.org account with System ID & API Key
2. Hardware Connection
- Wire RS485 A/B on inverter to adapter’s A/B.
- Plug adapter into your host’s USB port.
- Confirm device node (e.g.
/dev/ttyUSB0
) viadmesg | grep ttyUSB
.
3. Clone and Install
# Update system packages
sudo apt-get update && sudo apt-get install -y git python-serial
# (Raspberry Pi only) Optional firmware update
# sudo apt-get install rpi-update && sudo rpi-update && sudo reboot
# Clone repository
git clone https://github.com/stik79/DeltaPVOutput.git ~/DeltaPVOutput
cd ~/DeltaPVOutput
# (Optional) Install to /usr/local/bin
sudo cp *.py /usr/local/bin/
4. Configure
Copy and edit the example:
cd ~/DeltaPVOutput
cp config.py.example config.py
nano config.py
At minimum adjust:
# config.py
SYSTEM_ID = '12345' # from PVOutput.org Settings → System
API_KEY = 'abcdef0123456789'
SERIAL_PORT= '/dev/ttyUSB0' # your RS485 adapter
BAUD_RATE = 9600 # inverter default
Optional: enable MySQL logging by filling MYSQL_*
fields.
5. First Data Upload
Run the script manually to verify:
cd ~/DeltaPVOutput
python DeltaPVOutput.py
Successful run prints inverter metrics and “Upload successful” to console.
Check your PVOutput dashboard for a new entry.
6. Automate with Cron
Automate 5-minute uploads:
Create a wrapper to ensure correct paths:
cat > ~/DeltaPVOutput/run.sh <<'EOF' #!/bin/sh cd /home/pi/DeltaPVOutput /usr/bin/env python DeltaPVOutput.py EOF chmod +x ~/DeltaPVOutput/run.sh
Edit user crontab:
crontab -e
Add:
*/5 * * * * /home/pi/DeltaPVOutput/run.sh >> /home/pi/DeltaPVOutput/cron.log 2>&1
Verify:
crontab -l tail -f ~/DeltaPVOutput/cron.log
Logs show periodic data fetches and uploads. Adjust interval to meet PVOutput’s 60 submissions/day limit.
Configuration
Define application settings in config.py
(copy from config.py.example
). The collector reads this class on startup to configure inverter polling, PVOutput uploads, and optional MySQL logging.
1. Setup
- Copy and rename the example:
cp config.py.example config.py
- Place
config.py
alongside your data‐collector script. - Edit values described below, then restart the collector.
2. Configuration Options
Inverter & PVOutput.org
- RS485IDS
List of integer IDs matching each inverter’s RS485 address.RS485IDS = [1, 2]
- SYSTEMIDS
List of PVOutput.org system IDs corresponding to each inverter.SYSTEMIDS = ["123456", "123457"]
- TOTALSYSTEMID (optional)
PVOutput.org system ID for aggregated output (e.g. combined average).TOTALSYSTEMID = "123456"
- APIKEY
Your PVOutput.org write API key (keep secret).APIKEY = "abcd1234efgh5678ijkl9012mnop3456qrst"
If you run a single inverter, set SYSTEMIDS = ["123456"]
and omit TOTALSYSTEMID
.
Serial Communication
- serialBaud
RS485 bus baud rate (must match inverter). Default:19200
. - serialTimeoutSecs
Read timeout in seconds. Use ≥ 0.05 s to avoid empty reads. Default:0.1
.
serialBaud = 19200
serialTimeoutSecs = 0.1
MySQL Logger (Optional)
Enable local logging of each measurement by supplying database credentials. If mysqlUser
is empty or None
, the inserter skips writes.
- mysqlHost – Database host (e.g.
"localhost"
). - mysqlUser – Username with INSERT privileges on
deltapv.Measurement
. - mysqlPw – Password for
mysqlUser
. - mysqlDb – Database name, default
"deltapv"
.
mysqlHost = 'localhost'
mysqlUser = 'pvuser'
mysqlPw = 'pvpass'
mysqlDb = 'deltapv'
Before enabling, run the schema script:
mysql -u root -p < sql/createSchema.sql
3. Example config.py
class Configuration:
# Inverter RS485 IDs
RS485IDS = [1, 2]
# PVOutput.org credentials
SYSTEMIDS = ["123456", "123457"]
TOTALSYSTEMID = "123456"
APIKEY = "abcd1234efgh5678ijkl9012mnop3456qrst"
# Serial bus parameters
serialBaud = 19200
serialTimeoutSecs= 0.1
# Optional MySQL logging
mysqlHost = 'localhost'
mysqlUser = 'pvuser'
mysqlPw = 'pvpass'
mysqlDb = 'deltapv'
4. Practical Tips
- Keep
APIKEY
out of version control. - Verify
RS485IDS
on each inverter’s front panel. - If PVOutput uploads fail, check
SYSTEMIDS
and network access. - Monitor collector logs for MySQL connection errors when enabling the logger.
Usage
This section shows how to run the main script, schedule automated uploads with cron, and use helper utilities for bulk correction or real-time inspection.
Running the Main Script (DeltaPVOutput.py)
Ensure you have configured config.py
with:
serialPort
,serialBaud
,serialTimeoutSecs
SYSTEMIDS
: list of PVOutput system IDs (one per inverter)APIKEY
,TOTALSYSTEMID
- MySQL credentials for logging
Invoke the uploader:
cd /path/to/soreva/DeltaPVOutput
python3 DeltaPVOutput.py
By default it:
- Opens your RS-485 link (e.g.
/dev/ttyUSB0
). - Queries each inverter for power/energy/voltage/temperature.
- Posts individual inverter and total-system status to PVOutput.org.
- Logs raw inverter data into MySQL.
Enable verbose logging by editing the script’s print/log calls or redirecting stdout:
python3 DeltaPVOutput.py >> /var/log/DeltaPVOutput.log 2>&1
Scheduling with Cron
Automate data uploads every 5 minutes:
*/5 * * * * /usr/bin/python3 /path/to/DeltaPVOutput.py \
>> /var/log/DeltaPVOutput.log 2>&1
Automate nightly bulk checks at 1:00 AM:
0 1 * * * /usr/bin/python3 /path/to/bulkuploader.py \
--date $(date +\%Y\%m\%d) >> /var/log/bulkuploader.log 2>&1
Bulk-Upload Corrections (bulkuploader.py)
Use checkSystemDate
to sync your DB’s daily Wh totals with PVOutput.org:
# bulk_sync.py
from bulkuploader import checkSystemDate
from config import Configuration
from your_db_module import get_daily_wh
date = "20250910"
for idx, sys_id in enumerate(Configuration.SYSTEMIDS):
db_wh = get_daily_wh(inverter_id=idx, date=date)
# Fetch PVOutput’s record and POST any missing Wh
checkSystemDate(date, sys_id, db_wh)
Or run the built-in script (adjust flags if implemented):
python3 bulkuploader.py --date 20250910
Practical tips:
- Run after nightly DB aggregation to avoid missing data.
- Check logs for “PV is missing data!” to confirm POSTs.
Real-Time Inspection (power-now.py)
Quickly display live metrics and insert into MySQL:
python3 power-now.py
Sample output:
[Inv 1] PAC_AC=2450W V_DC=320V I_DC=7.6A Eff=95.6% EnergyDay=5300Wh
[Inv 2] PAC_AC=2190W V_DC=318V I_DC=6.9A Eff=94.5% EnergyDay=5120Wh
Adjust polling interval or iterate in a loop:
# loop_power.py
import time, power_now
while True:
power_now.main()
time.sleep(60)
Debugging Connectivity (debug.py)
Poll all registers and print to stdout for troubleshooting:
- Configure
Configuration.RS485IDS
and serial settings. - Run:
python3 debug.py
- Review output or errors:
-------- Inverter 1 -------- PAC_AC: 2450 W V_AC_L1: 230.1 V I_AC_L1: 6.3 A ... ######### Error getting data from inverter. Command: I_AC_L2 exception: TimeoutError
Filter registers by editing the loop in debug.py
:
for cmd in ("PAC_AC", "V_AC_L1", "I_AC_L1"):
val = inv.call(cmd)
print(f"{cmd}: {val} {inv.unit(cmd)}")
Free the serial port and increase serialTimeoutSecs
if you see frequent timeouts.
Architecture & Extending
This section describes how the code communicates with Delta inverters over RS-485/serial, ensures data integrity via CRC-16, and shows how to extend the framework for new inverter models or storage backends.
Subclassing Inverter: Adding a New Model
Purpose
Extend the core Inverter
base class to support a new hardware model by defining its cmds
list and using call()
to fetch data.
Essential Information
• Every inverter subclass sets a class-level cmds
list.
• Each cmds
entry is a 6-tuple:
- code (2-byte string): raw instruction
- name (string): key for
call(name)
- fmt (int): unpack format code
- divisor (float): scale factor
- unit (string): unit label (optional)
- response_size (int): expected response length
Format codes
0 – Unsigned 16-bit
1 – ASCII string
2 – Unsigned 32-bit
3 – Unsigned 64-bit
4 – Signed 16-bit
9 – Model string
10 – Firmware version
11 – Date (DD/MM-20YY)
12 – Time (HH:MM:SS)
13 – Alternate date
Code Example
from Inverter import Inverter
class MyCustomInv(Inverter):
cmds = [
# code, name, fmt, divisor, unit, response_size
[b'\x01\x10', 'Battery Voltage', 0, 100.0, 'V', 11],
[b'\x01\x11', 'Battery Current', 4, 1.0, 'A', 11],
[b'\x01\x20', 'Serial Number', 1, 0.0, '', 20],
[b'\x02\x00', 'FW Version', 10, 0.0, '', 12],
]
import serial
from config import Configuration
# 1. Open RS-485 serial connection
conn = serial.Serial(
'/dev/ttyUSB0',
Configuration.serialBaud,
timeout=Configuration.serialTimeoutSecs,
parity=serial.PARITY_EVEN,
rtscts=True,
xonxoff=True
)
# 2. Instantiate subclass (inverter ID = 1)
inv = MyCustomInv(1, conn)
# 3. Fetch and print values
voltage = inv.call('Battery Voltage') # e.g. "48.12"
print(f"{voltage} V")
serial_num = inv.call('Serial Number') # raw ASCII
print("S/N:", serial_num)
Practical Tips
• Verify response_size
matches your device spec—mismatch triggers CRC or length errors.
• Set divisor
to convert raw units (e.g., divide mV by 1000).
• Signed values (fmt 4) handle negatives automatically.
• call()
retries up to 20 times on failure with backoff and port reset.
• Enable the debug print in __unpackData
to inspect raw bytes.
CRC16 Class: Computing CRC-16 Checksums
Purpose
Provide one-shot and incremental CRC-16 calculations for DF1 (default) and Modbus protocols using a lookup table.
Key Constants
CRC16.INITIAL_DF1 = 0x0000
CRC16.INITIAL_MODBUS = 0xFFFF
Methods
• calcString(data: bytes, crc=INITIAL_DF1) → int
• calcByte(ch: int or bytes, crc: int) → int
Usage Examples
- One-Shot DF1 CRC
from crc import CRC16
crc16 = CRC16()
data = b"Hello, World!"
final_crc = crc16.calcString(data)
print(hex(final_crc)) # e.g. 0x4a17
- One-Shot Modbus CRC
from crc import CRC16
crc16 = CRC16()
payload = b"\x01\x03\x00\x00\x00\x0A"
crc = crc16.calcString(payload, CRC16.INITIAL_MODBUS)
low = crc & 0xFF
high = (crc >> 8) & 0xFF
frame = payload + bytes([low, high])
- Incremental CRC on Streams
from crc import CRC16
crc16 = CRC16()
crc = CRC16.INITIAL_MODBUS
for chunk in [b'\x01\x03\x00', b'\x00\x00\x0A']:
for b in chunk: # iterating bytes yields ints in Python 3
crc = crc16.calcByte(b, crc)
print(hex(crc))
- Mixing Bulk and Byte-wise
crc = crc16.calcString(large_block)
for b in footer_bytes:
crc = crc16.calcByte(b, crc)
Tips
• Mask final result with crc & 0xFFFF
.
• Use INITIAL_DF1
for DF1, INITIAL_MODBUS
for Modbus.
Defining Inverter Commands (cmds
array)
Purpose
Detail the structure of cmds
entries so Inverter.call(name)
can build, send, and parse commands.
Entry Structure
- code (2-byte string)
- name (string)
- fmt (int)
- divisor (float)
- units (string)
- response_size (int)
Under the Hood
• findCmdObj(name)
locates the entry.
• __buildCmd(code)
wraps code with STX, length, CRC16, ETX.
• connection.write()
sends it.
• __unpackData(response, cmdObj)
applies fmt
and divisor
.
Example: Adding a Humidity Command
class Delta30EU_G4_TR_Inverter(Inverter):
cmds = [
# existing entries...
[b'\x03\x03', 'Ambient Humidity', 0, 10.0, '%', 11],
]
import serial
from config import Configuration
from delta30EUG4TRInv import Delta30EU_G4_TR_Inverter
conn = serial.Serial(
'/dev/ttyUSB0',
Configuration.serialBaud,
timeout=Configuration.serialTimeoutSecs,
parity=serial.PARITY_EVEN,
rtscts=True,
xonxoff=True
)
inv = Delta30EU_G4_TR_Inverter(1, conn)
humidity = inv.call('Ambient Humidity') # e.g. "45.2"
print(f"{humidity} %")
Practical Tips
• Keep response_size
minimal to avoid read blocks.
• Ensure fmt
matches your inverter’s protocol manual.
• Use descriptive name
to improve code readability.
MysqlInserter.insert: Persisting Inverter Measurements to MySQL
Purpose
Record inverter readings into a MySQL Measurement
table; skips insert if credentials are missing.
How It Works
• Reads credentials from config.Configuration
.
• Connects via MySQLdb.
• Logs current row count.
• Converts values to int
, defaults to -1
on failure.
• Executes a parameterized INSERT
with rollback on error.
Prerequisites
In config.py
, set:
Configuration.mysqlHost
Configuration.mysqlUser
Configuration.mysqlPw
Configuration.mysqlDb
Ensure the Measurement
table has columns (inverterId, dcVoltage, dcPower, acPower)
.
Usage Example
from mysql import MysqlInserter
from delta30EUG4TRInv import Delta30EU_G4_TR_Inverter
import serial
from config import Configuration
# Initialize inserter and inverter
inserter = MysqlInserter()
conn = serial.Serial(
'/dev/ttyUSB0',
Configuration.serialBaud,
timeout=Configuration.serialTimeoutSecs,
parity=serial.PARITY_EVEN,
rtscts=True,
xonxoff=True
)
inv = Delta30EU_G4_TR_Inverter(1, conn)
# Fetch and store periodically
data = inv.read() # returns dict with keys 'id', 'dcV', 'dcP', 'acP'
inserter.insert(
inverterId=data['id'],
dcVoltage=data['dcV'],
dcPower=data['dcP'],
acPower=data['acP']
)
Practical Tips
• Wrap insert()
in retry logic for unstable networks.
• Monitor console logs for row counts and error messages.
• To add timestamps or extra columns, extend the SQL in insert()
and pass extra parameters.