Chat about this codebase

AI-powered code exploration

Online

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

  1. Serial Communication
    Inverter.py opens the RS485 port, builds request frames, validates and parses responses.
  2. Data Collection
    DeltaPVOutput.py reads configuration, instantiates Inverter clients for each address, and retrieves raw readings.
  3. Aggregation & Logging
    Script aggregates total power/energy, inserts per-inverter and system rows into MySQL.
  4. PVOutput.org Upload
    DeltaPVOutput.py formats data for the PVOutput API and issues HTTP POSTs for each inverter and the system total.
  5. 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

  1. Wire RS485 A/B on inverter to adapter’s A/B.
  2. Plug adapter into your host’s USB port.
  3. Confirm device node (e.g. /dev/ttyUSB0) via dmesg | 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:

  1. 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
    
  2. Edit user crontab:

    crontab -e
    

    Add:

    */5 * * * * /home/pi/DeltaPVOutput/run.sh >> /home/pi/DeltaPVOutput/cron.log 2>&1
    
  3. 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

  1. Copy and rename the example:
    cp config.py.example config.py
    
  2. Place config.py alongside your data‐collector script.
  3. 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:

  1. Opens your RS-485 link (e.g. /dev/ttyUSB0).
  2. Queries each inverter for power/energy/voltage/temperature.
  3. Posts individual inverter and total-system status to PVOutput.org.
  4. 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:

  1. Configure Configuration.RS485IDS and serial settings.
  2. Run:
    python3 debug.py
    
  3. 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:

  1. code (2-byte string): raw instruction
  2. name (string): key for call(name)
  3. fmt (int): unpack format code
  4. divisor (float): scale factor
  5. unit (string): unit label (optional)
  6. 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

  1. 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
  1. 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])
  1. 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))
  1. 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

  1. code (2-byte string)
  2. name (string)
  3. fmt (int)
  4. divisor (float)
  5. units (string)
  6. 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.