Technical

Solar Energy Optimization: RS485 Inverter Hacking for Load Management

Reverse engineering solar inverter protocols and building a Python-based energy management system that dynamically controls loads based on solar production and grid demand—cutting electricity costs without sacrificing comfort.

Automation Services Team

Solar Energy Optimization: RS485 Inverter Hacking for Load Management

A client with a 15kW rooftop solar system was frustrated: they had plenty of solar capacity, but their energy bills were still high because large loads (pool pumps, hot water heaters, air conditioning) ran during peak grid pricing. Their inverter could produce the power, but usage timing was all wrong.

We built a Python-based energy management system that interfaces directly with their inverter via RS485, monitors real-time solar production and grid demand, and intelligently controls loads to maximize solar utilization and minimize grid consumption during expensive peak periods.

The Problem: Time-of-Use Pricing and Load Timing

Modern electricity pricing isn't flat. Time-of-use (TOU) tariffs charge different rates throughout the day:

  • Peak (4pm-9pm): $0.45/kWh
  • Shoulder (7am-4pm, 9pm-10pm): $0.28/kWh
  • Off-peak (10pm-7am): $0.15/kWh

Solar production peaks midday (10am-3pm) when demand is in the shoulder period. Meanwhile, the pool pump ran from 8am-4pm regardless of solar production, and the hot water system heated overnight using expensive off-peak power instead of free solar at noon.

The opportunity: Shift controllable loads to match solar production, and avoid all loads during peak pricing.

System Architecture: Monitoring and Control

The solution required three capabilities:

  1. Monitor inverter production (real-time solar generation)
  2. Monitor grid consumption (current draw from/to grid)
  3. Control loads (turn equipment on/off based on conditions)

Hardware Components

  • Raspberry Pi 4: Central controller running Python automation loop
  • Waveshare RS485 HAT: Interface inverter Modbus protocol
  • Energy meters (SDM630): Three-phase grid consumption monitoring
  • Smart relays (Shelly Pro 4PM): Load switching with power monitoring
  • Solar inverter (GoodWe GW5048D-ES): 5kW hybrid inverter with battery backup

All components communicate over Modbus RTU (RS485) or Modbus TCP, providing a unified monitoring and control interface.

Reverse Engineering the Inverter Protocol

Solar inverters don't publish full protocol documentation. The GoodWe inverter had basic Modbus registers documented, but we needed deeper access to control charging behavior and read internal state.

Modbus Basics

Modbus is a simple master/slave protocol commonly used in industrial automation:

Master sends: [Device ID] [Function Code] [Register Address] [Checksum]
Slave responds: [Device ID] [Function Code] [Data] [Checksum]

Function codes we used:

  • 0x03: Read holding registers (production data, battery SOC, etc.)
  • 0x04: Read input registers (real-time power flow)
  • 0x10: Write multiple registers (control settings)

Protocol Discovery Process

We captured RS485 traffic between the inverter and its monitoring WiFi dongle using a logic analyzer, then decoded the Modbus frames:

Read solar production:

Query:  01 04 00 64 00 02 [CRC]
        └─ Device 1, Function 4, Register 100, Count 2

Response: 01 04 04 13 88 00 00 [CRC]
          └─ 0x1388 = 5000W current production

Read battery state:

Query:  01 03 00 6B 00 01 [CRC]
Response: 01 03 02 00 5A [CRC]
          └─ 0x005A = 90% SOC

We built a register map by systematically reading memory ranges and correlating values with known inverter states (measured with clamp meters).

Python Modbus Library: minimalmodbus

For implementation, we used minimalmodbus library:

import minimalmodbus

inverter = minimalmodbus.Instrument('/dev/ttyUSB0', 1)  # Device 1 on RS485
inverter.serial.baudrate = 9600
inverter.serial.timeout = 1

def get_solar_production():
    """Read current solar production in watts"""
    production_w = inverter.read_register(100, functioncode=4)
    return production_w

def get_grid_power():
    """Read grid power (+ = importing, - = exporting)"""
    grid_w = inverter.read_register(110, functioncode=4, signed=True)
    return grid_w

def get_battery_soc():
    """Read battery state of charge (%)"""
    soc = inverter.read_register(107, functioncode=3)
    return soc

This provides clean Python access to all inverter telemetry.

Grid Power Monitoring: SDM630 Energy Meters

The inverter reports grid power, but we wanted independent verification and per-phase monitoring. We installed Eastron SDM630 three-phase energy meters at the main switchboard.

Modbus TCP Integration

The SDM630 meters have Ethernet Modbus TCP gateways:

from pymodbus.client import ModbusTcpClient

meter = ModbusTcpClient('192.168.1.50', port=502)

def get_grid_consumption():
    """Read total power consumption across all phases"""
    result = meter.read_input_registers(0x0034, 2, unit=1)  # Total system power
    power_w = struct.unpack('>f', struct.pack('>HH', *result.registers))[0]
    return power_w

def get_phase_currents():
    """Read current on each phase"""
    phase_a = meter.read_input_registers(0x0006, 2, unit=1)
    phase_b = meter.read_input_registers(0x0008, 2, unit=1)
    phase_c = meter.read_input_registers(0x000A, 2, unit=1)
    
    return {
        'A': struct.unpack('>f', struct.pack('>HH', *phase_a.registers))[0],
        'B': struct.unpack('>f', struct.pack('>HH', *phase_b.registers))[0],
        'C': struct.unpack('>f', struct.pack('>HH', *phase_c.registers))[0]
    }

This provides real-time visibility into what's actually happening at the grid connection point.

Load Control: Smart Relays and Power Monitoring

We installed Shelly Pro 4PM relay modules to control major loads:

  • Pool pump (1.5kW)
  • Hot water heater (3.6kW)
  • Workshop heater (2.4kW)
  • EV charger (7.2kW adjustable)

Shelly HTTP API

The Shelly devices expose a simple HTTP API:

import requests

def set_relay_state(device_ip, relay_id, state):
    """Turn relay on or off"""
    url = f"http://{device_ip}/relay/{relay_id}"
    params = {'turn': 'on' if state else 'off'}
    response = requests.get(url, params=params)
    return response.json()

def get_relay_power(device_ip, relay_id):
    """Get current power consumption of load"""
    url = f"http://{device_ip}/status"
    response = requests.get(url)
    status = response.json()
    return status['meters'][relay_id]['power']  # Watts

Each relay module reports power consumption, so we can verify loads are actually drawing expected power (detecting equipment failures).

The Control Logic: Python Automation Loop

The core system runs as a Python daemon, executing decision logic every 30 seconds:

def control_loop():
    while True:
        # Read current state
        solar_w = get_solar_production()
        grid_w = get_grid_consumption()
        battery_soc = get_battery_soc()
        hour = datetime.now().hour
        
        # Calculate available solar capacity
        loads_w = sum(get_relay_power(device, i) for device, i in CONTROLLED_LOADS)
        available_solar_w = solar_w - grid_w - loads_w
        
        # Determine pricing period
        pricing_period = get_pricing_period(hour)
        
        # Make control decisions
        if pricing_period == 'peak':
            # NEVER run optional loads during peak pricing
            disable_all_optional_loads()
        
        elif available_solar_w > 2000:  # 2kW+ excess solar
            # Enable loads in priority order
            if not is_hot_water_heated():
                enable_load('hot_water_heater')
            elif not has_pool_run_today():
                enable_load('pool_pump')
            # ... additional loads in priority order
        
        elif battery_soc < 30 and solar_w > 1000:
            # Low battery, good solar - prioritize charging
            disable_optional_loads()
        
        elif grid_w > 500:  # Importing from grid
            # We're consuming more than solar produces
            # Disable lowest-priority loads
            disable_lowest_priority_load()
        
        time.sleep(30)

This implements a priority system where loads are enabled/disabled based on available solar capacity and pricing periods.

Priority-Based Load Management

Not all loads are equal. We created a priority system:

Priority 1: Critical Loads (Never Automated)

  • Refrigeration
  • Lighting
  • Computers/network equipment

Priority 2: High-Value Comfort (Solar Preferred)

  • Air conditioning (only during solar availability if possible)
  • Hot water heating (prefer solar, fall back to off-peak if needed)

Priority 3: Deferrable Loads (Solar Only)

  • Pool pump (must run daily, timing flexible)
  • Battery charging (top-up during excess solar)

Priority 4: Optional Loads (Excess Solar Only)

  • Workshop heater (nice-to-have)
  • EV charging (can use off-peak as fallback)

The control loop evaluates priorities and enables loads starting from highest priority when solar is available.

Battery Management: Prevent Grid Charging

The inverter had a frustrating behavior: it would charge the battery from the grid during off-peak periods (when power is cheap) to enable peak-period discharge (when power is expensive).

Sounds smart, but:

  • Grid charging efficiency is ~90%: You lose 10% in the round-trip
  • Battery cycle life cost: Each charge/discharge cycle has wear cost
  • We had excess solar: No need to grid-charge when we can solar-charge

We disabled automatic grid charging via Modbus write:

def disable_grid_charging():
    """Disable grid charging, allow solar only"""
    # Register 0x1032: Battery charging source
    # 0 = Solar only, 1 = Solar + Grid
    inverter.write_register(0x1032, 0, functioncode=16)

This immediately improved efficiency—battery now charges from excess solar instead of expensive grid power.

Peak Shaving: Battery Discharge During Peak

While we disabled grid charging, we enabled intelligent battery discharge:

def peak_shaving_logic():
    hour = datetime.now().hour
    battery_soc = get_battery_soc()
    grid_w = get_grid_consumption()
    
    if 16 <= hour < 21:  # Peak pricing period (4pm-9pm)
        if grid_w > 1000 and battery_soc > 30:
            # We're importing during peak AND battery has charge
            # Force battery discharge to reduce grid import
            set_battery_mode('discharge', power_w=min(grid_w, 3000))
        else:
            # Let battery idle (natural discharge as loads require)
            set_battery_mode('auto')
    else:
        # Outside peak, standard operation
        set_battery_mode('auto')

This uses stored solar energy to reduce grid consumption during the most expensive period.

Real-World Results: Cost Savings

After 6 months of operation, the numbers spoke for themselves:

Energy Cost Comparison

Before optimization:

  • Average daily grid import: 25kWh
  • Peak period consumption: 8kWh @ $0.45/kWh = $3.60/day
  • Monthly electricity cost: ~$280

After optimization:

  • Average daily grid import: 12kWh
  • Peak period consumption: 1.5kWh @ $0.45/kWh = $0.68/day
  • Monthly electricity cost: ~$140

Savings: $140/month, $1,680/year

System cost (hardware + development): ~$1,500 Payback period: 11 months

Solar Utilization Improvement

  • Before: ~60% of solar production exported to grid @ $0.08/kWh
  • After: ~85% of solar production self-consumed @ $0.28 effective value

The system effectively increased the value of each kWh of solar production by ~3x through improved timing.

Unexpected Benefits: Equipment Longevity

Secondary benefits emerged:

Reduced HVAC Cycling

Air conditioning now runs during peak solar production (10am-3pm) to pre-cool the home, then coasts through the expensive peak period (4pm-9pm) with minimal cycling. This reduced compressor starts and improved efficiency.

Hot Water System Efficiency

Electric hot water heating during midday solar (when it's warmer) is more efficient than nighttime off-peak heating (when it's cold and heat loss is higher). We saw ~8% improvement in hot water system efficiency.

Pool Pump Runtime Optimization

Instead of running 8 hours straight, the pool pump now runs in two 2-hour blocks during peak solar periods with a break in between. This improved filtration efficiency (allowing settled debris to be filtered twice) while reducing total runtime.

Monitoring and Visualization

We integrated the system with Grafana for visibility:

Dashboard metrics:

  • Solar production vs. consumption (real-time and historical)
  • Grid import/export with pricing overlay
  • Battery SOC and charge/discharge rates
  • Individual load status and power consumption
  • Cost tracking (daily/monthly actual vs. projected without optimization)

This transparency let the homeowner understand exactly how the system was performing and where the savings came from.

Challenges and Solutions

RS485 Reliability Issues

Initial deployment had intermittent Modbus communication failures. Root causes:

  • Cable length: RS485 spec is 1200m, but we had poor-quality cable
  • Termination resistors: Missing 120Ω termination at bus ends
  • Ground loops: Multiple ground references causing noise

Solutions:

  • Replaced with twisted-pair shielded cable (Cat6)
  • Added 120Ω termination resistors at inverter and meter ends
  • Implemented retry logic with exponential backoff in Python code

After fixes, communication reliability improved to 99.9%+.

Load Priority Conflicts

Some loads had hard dependencies:

  • Pool pump must run 4+ hours/day minimum (algae prevention)
  • Hot water must be heated daily (health/comfort)

If solar was insufficient for multiple days (overcast weather), the system needed fallback logic:

def enforce_minimum_runtime():
    """Ensure critical loads run even if solar is unavailable"""
    if pool_pump_runtime_today < 4 * 3600:  # 4 hours in seconds
        if datetime.now().hour >= 22:  # 10pm, off-peak starts
            # Force pool pump run using cheap off-peak power
            enable_load('pool_pump', forced=True)

This prevents the optimization system from sacrificing essential services.

Future Enhancements

The system architecture supports additional features:

Weather Forecast Integration

Pull next-day solar forecast from weather APIs and pre-charge battery if low production is predicted.

Grid Export Optimization

In markets with variable export tariffs, optimize battery discharge timing to export during highest-paying periods.

Machine Learning Load Prediction

Train models on historical usage to predict load requirements and preemptively adjust battery charging.

Vehicle-to-Grid (V2G) Integration

When EV supports bidirectional charging, use EV battery as additional storage to support home during peak periods.


Want to maximize your solar investment? We design and implement custom energy management systems that interface with existing infrastructure to optimize consumption, reduce costs, and improve system efficiency. Contact us to discuss your energy optimization requirements.