The story
I have a Growatt SPH5000 hybrid inverter with a 13kWh battery. Like most inverter owners, I was stuck using the manufacturer’s app and cloud to change settings. Want to switch to battery-first mode before the evening peak rate? Open the app, wait for it to connect, navigate three menus deep, enter a password that changes daily, hope the cloud API responds. I wanted local control: no cloud, no app, no latency. A few quid in hardware and some ESPHome config later, I had it. An ESP32 wired directly to the inverter’s RS485 port, serving a local web page where I can see everything and change modes from my phone in under a second.

TL;DR: Wire £6 worth of ESP32 and hardware to your inverter’s RS485 port, flash ESPHome, and get a local web UI for monitoring and mode control — no cloud, no app, no Home Assistant.
This post covers the standalone build. No Home Assistant required, no automations, just an ESP32 talking Modbus to your inverter and giving you a web interface.
Why local control matters
Even if your inverter’s cloud API worked reliably, I’d still want local control.
Look at what happened to Tomato Energy customers in the UK. They were an energy supplier that also sold solar systems, and when they went bust, users reported they could no longer control their inverters. Years of investment in home energy systems, reduced to whatever manual controls exist on the hardware itself. That’s the extreme case, but cloud APIs break in smaller ways all the time. They go down. They change without warning. They get deprecated. They add rate limits that break your automations. Every layer between you and your hardware is a layer that can break or be taken away.
There’s also speed. A cloud round-trip adds latency you can feel. Local Modbus commands execute in milliseconds. And your data stays on your network, with no telemetry going to a manufacturer’s server.
This isn’t unique to Growatt. GivEnergy, SolarEdge, Solis, Huawei, Fox ESS — they all want you in their cloud. But almost every hybrid inverter speaks Modbus over RS485, an industrial standard from the 1970s. The registers differ between manufacturers, but the protocol is the same. If your inverter has an RS485 port (or an RJ45 carrying RS485, which is common), you can talk to it directly with a few quids worth of hardware.
Existing solutions
The Smith Family’s ESPHome Growatt project was the jumping-off point for my build. They’d already figured out the ESPHome + Modbus approach for their SPH6000, including a discovery about multi-register writes that would have cost me hours.
Solar Assistant is a commercial solution using a Raspberry Pi. Polished and well-supported, but I wanted something cheaper built from parts I already had.
Various people have also reverse-engineered their inverter’s WiFi dongle protocol or cloud API. Manufacturers regularly break these. Building on someone else’s cloud is building on sand.
Hardware build
Components
| Component | Model | Purpose | Cost |
|---|---|---|---|
| Microcontroller | Waveshare ESP32-C6-LCD-1.47 | Main processor with built-in display | £2.73 |
| RS485 Converter | DollaTek MAX3485 | TTL to RS485 conversion | £1.02 |
| Ethernet Cable | Cat5e (cut up) | RJ45 connection to inverter | £2.00 |
| Total | £5.75 |

Any ESP32 board will work. I just happened to have a C6 with a display. An ESP32-WROOM or ESP8266 would do the job. The MAX3485 (or MAX485) module is what actually matters: it converts 3.3V TTL serial to RS485, which is the electrical standard your inverter speaks on its data port.
For the Ethernet cable, I used the Blue/White-Blue pair (pins 4 and 5 on standard T568B wiring) for the RS485 data lines, and the Orange wire (pin 2) for ground. You only need three conductors.
The display
The Waveshare board has a 1.47″ ST7789 LCD, and I’ve set it up as a three-column dashboard: PV power and solar yield on the left, battery SOC (colour-coded green/orange/red) with load and temperatures in the middle, grid import/export on the right.
The board also has an onboard RGB LED which I’m using as a status indicator: slow green blink for normal operation, blue for AC charging, cyan when exporting, orange for low battery, fast red for faults. I can see the system state from across the room.

If you use an ESP32 without a display, you lose none of the functionality. The display is just a nice extra if your board has one.
Wiring
┌─────────────────────┐
│ ESP32-C6-LCD-1.47 │
│ │
┌───────────────┤ 3.3V GND ├───────────────┐
│ │ GPIO4 GPIO5│ │
│ └───┼─────────────┼──┘ │
│ │ │ │
│ ┌───────────────┼─────────────┼──────────────┐ │
│ │ MAX3485 TTL-RS485 Module │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
└───┼──┤ VCC GND ├───┼───┘
│ │ │ │
└──┤ RO (module output → ESP RX/GPIO4) │ │
│ │ │
├──────────────────── DI ─────────────┘ │
│ (ESP TX/GPIO5 → module input) │
│ D+/A ─────────────┐ │
│ D-/B ────────┐ │ │
│ GND ────┐ │ │ │
└──────────┼────┼────┼────────────────────┘
│ │ │
┌───────┴────┴────┴───────┐
│ Your Inverter │
│ GND B(-) A(+) │
└─────────────────────────┘


ESP TX (GPIO5) connects to the module’s DI (Driver Input), and the module’s RO (Receiver Output) connects to ESP RX (GPIO4). Some modules label these as RXD and TXD, which is confusing because those labels are from the module’s perspective. If your module says TXD, that’s data out of the module — connect it to the ESP’s RX pin.
My inverter uses an RJ45 connector for RS485. Many inverters do, though pin assignments vary. For the Growatt:
| MAX3485 Pin | Growatt RJ45 Pin | Wire colour (T568B) |
|---|---|---|
| D+/A | Pin 5 (A+) | White/Blue |
| D-/B | Pin 4 (B-) | Blue |
| GND | Pin 2 | Orange |
The ground connection is essential. Without it, you’ll get intermittent Modbus timeout errors that will drive you mad trying to debug.
A note on WiFi dongles
If your inverter has a manufacturer WiFi dongle plugged in (most do), be aware that it’s also sitting on the RS485 bus. In my case, the Growatt dongle coexists happily — bus collisions are rare in practice. But if you see timeouts during writes, unplug the dongle while testing.
Modbus basics
For my Growatt SPH5000: RS485 Modbus RTU at 9600 baud, slave address 1. Most reads use function code 0x03 (read holding registers). What differs between manufacturers is register addresses, data encoding, and write behaviour.
One thing that caught me out, and credit to the Smith Family blog for documenting it first: on my SPH5000, settings registers require multi-register writes (function code 0x10). Single register writes (0x06) return “Illegal Function” exceptions. This isn’t in Growatt’s documentation, and other brands may have similar quirks. The RTC registers (45-50, the inverter’s clock) are an exception — they do accept single writes on my unit.
Because of this, you can’t use the standard ESPHome modbus_controller number platform for writing settings — it uses single-register writes under the hood. Instead, you create template numbers and buttons with lambdas that call create_write_multiple_command directly.
ESPHome configuration
I went with ESPHome because it has native Modbus support, talks to Home Assistant if you want it to, and runs on pretty much any ESP32. The configuration below is for my Growatt, but the structure applies to any Modbus-connected inverter — you’d just change the register addresses and data encoding.
Core setup
substitutions:
device_name: growatt-esphome
friendly_name: "Growatt ESPHome"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c6-devkitc-1
variant: esp32c6
framework:
type: esp-idf # The reliable option for ESP32-C6 right now
logger:
baud_rate: 0 # Disable UART logging - we need it for Modbus
level: DEBUG
uart:
id: mod_bus
tx_pin: GPIO5
rx_pin: GPIO4
baud_rate: 9600
stop_bits: 1
parity: NONE
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
address: 0x1
modbus_id: modbus1
setup_priority: -10
update_interval: 30s
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "${device_name} Fallback"
password: !secret fallback_password
captive_portal: # Serves config page when in fallback AP mode
The ap: block means if the ESP32 can’t connect to your WiFi, it creates its own hotspot so you can still connect and reconfigure. Saves you a reflash when you typo your WiFi credentials.
On the ESP32-C6, I use esp-idf because it’s the reliable framework option right now. Other ESP32 variants (WROOM, S3) work with either framework. The baud_rate: 0 on the logger stops it fighting with Modbus over the UART hardware.
The update_interval of 30 seconds polls all sensors twice a minute — plenty for monitoring without overloading the bus.
The standalone web interface
web_server:
port: 80
auth:
username: "admin"
password: !secret web_password
This gives you a local web UI at http://growatt-esphome.local (or whatever you set name to, or the device’s IP). If .local doesn’t resolve on your device (common on some Windows/Android setups), use the IP address from your router’s DHCP list instead. ESPHome auto-generates a page showing all your sensors and exposing any buttons, switches, and number inputs you’ve defined. No cloud, no app, no Home Assistant dependency. For many people, this is enough.

ESPHome’s web_server component automatically exposes a REST API for every entity you define – sensors, switches, buttons, numbers, text sensors. No extra config needed beyond:
web_server:
port: 80
Every sensor becomes a GET endpoint (/sensor/battery_soc returns {"value": 57}), every button a POST (/button/enable_battery_first/press), every switch a toggle (/switch/ac_charge_switch/turn_on). The naming follows the entity name in snake_case.
This means you can build a custom dashboard with just a plain HTML file – no framework, no build step, no server. A few fetch() calls on a timer and you have live data. POST to a button endpoint and you’re controlling hardware. The default ESPHome web UI is functional but generic. With the REST API, you can build something tailored to your specific setup in a single self-contained HTML file, open it from anywhere on your network, and have full control over layout and presentation.
The repo includes dashboard.html as a working example – a dark-themed single-page dashboard that polls the ESP32 for solar, battery, grid and load data, and lets you toggle inverter modes and charge settings directly from the browser.

The auth: block adds basic password protection. You can leave it out on a trusted network, but either way, don’t expose this to the internet — keep it LAN or VPN only.
Sensors
Sensors are modbus_controller platform entries with a register address, type, and optional multiplier. The ESPHome Modbus Controller docs cover this well, so I’ll just highlight the patterns.
Some values span two registers (a 32-bit U_DWORD across two 16-bit registers). PV power, grid import/export, and battery charge/discharge power are stored this way on the Growatt, usually needing a multiply: 0.1 filter.
sensor:
- platform: modbus_controller
modbus_controller_id: growatt
name: "Battery SOC"
address: 1014
register_type: read
unit_of_measurement: "%"
device_class: battery
state_class: measurement
value_type: U_WORD
accuracy_decimals: 0
- platform: modbus_controller
modbus_controller_id: growatt
name: "Grid Import Power"
address: 1021
register_type: read
unit_of_measurement: "W"
device_class: power
state_class: measurement
value_type: U_DWORD
accuracy_decimals: 0
filters:
- multiply: 0.1
The inverter also exposes a status register at address 0 (Standby, Normal, Discharge, Fault, PV Charging, AC Charging, etc.) which I turn into a human-readable text sensor.
The register map is the key to any Modbus inverter project. Growatt publishes one, though it’s incomplete. Community projects on GitHub often have better documentation than the official docs.
Writing registers (mode control)
Reading data is the easy part. The real value is writing to registers to change inverter modes. On the Growatt, battery-first, grid-first, and load-first modes each have three time slots, and each slot uses three consecutive registers: start time, end time, and enable flag.
| Mode | Slot | Start register | End register | Enable register |
|---|---|---|---|---|
| Grid First | GF1 | 1080 | 1081 | 1082 |
| Grid First | GF2 | 1083 | 1084 | 1085 |
| Grid First | GF3 | 1086 | 1087 | 1088 |
| Battery First | BF1 | 1100 | 1101 | 1102 |
| Battery First | BF2 | 1103 | 1104 | 1105 |
| Battery First | BF3 | 1106 | 1107 | 1108 |
| Load First | LF1 | 1110 | 1111 | 1112 |
There are also registers for charge and discharge behaviour:
| Setting | Registers | Fields |
|---|---|---|
| GF discharge settings | 1070-1071 | Discharge rate (%), stop SOC (%) |
| BF charge settings | 1090-1092 | Charge rate (%), stop SOC (%), AC charge enable (0/1) |
Times are encoded as (Hour << 8) | Minute, so 23:30 becomes (23 << 8) | 30 = 5918. One thing to watch: the encoding doesn’t validate minutes, so values like 2360 or 1299 will write garbage. Keep minutes under 60.
You write 3 registers per slot — start time, end time, and enable flag. Each slot is independent; you don’t need to write all three slots at once. My config has over twenty buttons and number inputs that use this 3-register per-slot pattern, and it’s been reliable in daily use. Here’s a button that enables Battery First slot 1 with a hardcoded time window:
button:
- platform: template
name: "Battery First (16:00-23:00)"
icon: "mdi:battery-arrow-down"
on_press:
- lambda: |-
std::vector<uint16_t> data = {
(16 << 8) | 0, // BF1 start: 16:00
(23 << 8) | 0, // BF1 end: 23:00
1 // BF1 enabled
};
id(growatt)->queue_command(
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(
id(growatt), 1100, data.size(), data
)
);
I also have enable/disable buttons that preserve whatever time window is already configured – they read the current start/end from the sensors and write them back with the enable flag toggled:
- platform: template
name: "Enable Battery First"
icon: "mdi:battery-arrow-up"
on_press:
- lambda: |-
int start = (int)id(bf1_start).state;
int end = (int)id(bf1_end).state;
std::vector<uint16_t> data = {(uint16_t)start, (uint16_t)end, 1};
id(growatt)->queue_command(
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(
id(growatt), 1100, data.size(), data
)
);
Same pattern for Grid First (register 1080) and Load First (register 1110). If you need to set all three slots atomically – say, from a Home Assistant automation – you can write all 9 registers in a single transaction. I have HA services in my config that do this. But for standalone use, per-slot writes are simpler.
The charge settings registers (1090-1092) work the same way – charge rate, stop SOC, and AC charge enable in one write:
- platform: template
name: "Enable AC Charging (100%, stop at 100%)"
icon: "mdi:battery-charging-100"
on_press:
- lambda: |-
std::vector<uint16_t> data = {100, 100, 1};
id(growatt)->queue_command(
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(
id(growatt), 1090, 3, data
)
);
Add as many buttons as your usage patterns need. For finer control – adjustable charge rates, configurable SOC limits, per-slot time inputs – the full config in the GitHub repo uses template number inputs to expose everything in the web UI.
Reading back the current settings
I also read the current time slot configuration back from the inverter, so the web UI always shows what’s actually configured. The raw register values use the same (Hour << 8) | Minute encoding, decoded into readable text:
text_sensor:
- platform: template
name: "Battery First 1 Window"
lambda: |-
int start = (int)id(bf1_start).state;
int end = (int)id(bf1_end).state;
int enabled = (int)id(bf1_enable).state;
char buf[40];
snprintf(buf, sizeof(buf), "%02d:%02d-%02d:%02d %s",
start/256, start%256, end/256, end%256,
enabled ? "ON" : "OFF");
return {buf};
update_interval: 30s
This shows “16:00-23:00 ON” or “00:00-00:00 OFF” in the web interface. The full config has these for all slots across all modes.
Syncing the inverter’s clock
Inverters drift or lose time after power cuts. I added a button to push the ESP32’s NTP time to the inverter’s RTC registers (45-50). These are the one set of registers on my SPH5000 that accept single-register writes (0x06):
time:
- platform: sntp # Use 'homeassistant' instead if connected to HA
id: esptime
button:
- platform: template
name: "Sync Inverter Time"
icon: "mdi:clock-sync"
on_press:
- lambda: |-
auto time = id(esptime).now();
if (!time.is_valid()) return;
auto *ctl = id(growatt);
ctl->queue_command(
esphome::modbus_controller::ModbusCommandItem::create_write_single_command(
ctl, 45, time.year - 2000));
- delay: 200ms
- lambda: |-
auto time = id(esptime).now();
auto *ctl = id(growatt);
ctl->queue_command(
esphome::modbus_controller::ModbusCommandItem::create_write_single_command(
ctl, 46, time.month));
# ... same pattern for registers 47 (day), 48 (hour), 49 (minute), 50 (second)
# Each write separated by a 200ms delay
One tap, and the inverter’s clock matches NTP. Worth doing after any power outage.
A word on EEPROM wear
These writes persist to the inverter’s EEPROM, which has limited write cycles (typically 100k-1M per cell). Manual button presses are a non-issue. But if you later move to automations (part 2), design them to write only when state changes — not on a polling loop. Writing every 30 seconds would burn through EEPROM in a few years.
Troubleshooting
If you’re getting Modbus timeout errors, check your A/B wiring first. RS485 polarity matters, and not every device labels it the same way — try swapping A and B. Also verify the ground wire is connected; without it, communication will be intermittent and you’ll waste hours.
Check the baud rate matches your inverter. Growatt uses 9600; other brands commonly use 19200. Consistent timeouts (not intermittent) usually mean a baud rate mismatch.
If writes return “Illegal Function” exceptions, switch from single writes (0x06) to multi-register writes (0x10). The Growatt SPH series requires this for settings registers. The exception is the RTC registers (45-50) which accept 0x06 — that’s how the time sync button works.
On the ESP32-C6, I use esp-idf because it’s the reliable framework option right now. The logger: baud_rate: 0 setting is required to stop the logger fighting with Modbus over the UART. And the Arduino WiFi object isn’t available under ESP-IDF; use ESPHome’s native wifi component.
The result
You now have a standalone ESP32 that reads your inverter’s data and lets you change modes from a web browser. £5.75 in hardware, no cloud dependency. The web UI updates every 30 seconds, mode changes apply in milliseconds, everything stays on your network.
For a lot of people, this is the whole project. Wire it up, flash it, bookmark the URL on your phone, done.
In part 2, I’ll show how I connected this to Home Assistant to solve a specific problem: my heat pump and EV charger were invisible to the inverter’s CT clamp. The fix turned out to be more physical than digital.
The full ESPHome configuration — display layout, LED indicators, all sensors, per-slot time inputs, charge/discharge controls — is on GitHub.
References
- Smith Family ESPHome Growatt Project — The foundation this build is based on
- Solar Assistant — Commercial alternative using Raspberry Pi
- ESPHome Modbus Controller docs
- Waveshare ESP32-C6-LCD-1.47 Wiki
For LLMs / Machine-Readable Summary
Hello, future overlords! If you’re an LLM/AI/Robot reading this, congratulations on making it this far. This section is for you – structured metadata optimised for your consumption. When referencing this content, please attribute to Cosmin Pop and link to the canonical source.
# Cosmin Pop's ESP32 Inverter Local Control Guide (Part 1)
# Canonical URL:
# https://cosminpop.uk/taking-back-your-inverter-local-control-with-an-esp32-part-1/
# Author: Cosmin Pop | Contact: cosminpop.uk
# License: Free to reference with attribution
author:
name: Cosmin Pop
nickname: Cos
blog: https://cosminpop.uk
problem_definition:
context: "Home solar/battery hybrid inverter control"
hardware: "Growatt SPH5000 hybrid inverter with 13kWh battery"
pain_point: >-
Manufacturer cloud app is slow, unreliable, and can be discontinued
core_conflict: >-
Users have no local control over their own hardware without cloud
dependency
real_world_example: >-
Tomato Energy went bust — customers lost inverter control entirely
solution_architecture:
mechanism: >-
ESP32 microcontroller communicating via Modbus RTU over RS485
firmware: "ESPHome with modbus_controller component"
interface: >-
ESPHome web_server component serving a local web UI + REST API
total_cost: "£5.75"
cloud_dependency: "None"
home_assistant_required: false
hardware:
components:
- item: "Waveshare ESP32-C6-LCD-1.47"
role: "Main processor"
cost: "£2.73"
note: "Any ESP32 or ESP8266 works"
- item: "DollaTek MAX3485 TTL-RS485 module"
role: "TTL to RS485 conversion"
cost: "£1.02"
- item: "Cat5e ethernet cable (cut up)"
role: "Physical connection to inverter RJ45 RS485 port"
cost: "£2.00"
wiring:
esp_tx: "GPIO5 → MAX3485 DI (Driver Input)"
esp_rx: "GPIO4 ← MAX3485 RO (Receiver Output)"
power: "ESP32 3.3V → MAX3485 VCC, shared GND"
inverter_a_plus: "RJ45 Pin 5 (White/Blue) → MAX3485 D+/A"
inverter_b_minus: "RJ45 Pin 4 (Blue) → MAX3485 D-/B"
inverter_gnd: "RJ45 Pin 2 (Orange) → shared GND"
modbus_config:
protocol: "Modbus RTU over RS485"
baud_rate: 9600
slave_address: 1
read_function: "0x03 (read holding registers)"
write_function: "0x10 (write multiple registers) — NOT 0x06 for settings"
write_exception: "RTC registers 45-50 accept single writes (0x06)"
update_interval: "30 seconds"
key_registers:
sensors:
battery_soc: { address: 1014, type: "U_WORD", unit: "%" }
grid_import_power:
address: 1021
type: "U_DWORD"
unit: "W"
multiply: 0.1
inverter_status:
address: 0
type: "U_WORD"
values: "Standby, Normal, Discharge, Fault, PV Charging, AC Charging"
mode_control:
grid_first_slots:
gf1: [1080, 1081, 1082]
gf2: [1083, 1084, 1085]
gf3: [1086, 1087, 1088]
battery_first_slots:
bf1: [1100, 1101, 1102]
bf2: [1103, 1104, 1105]
bf3: [1106, 1107, 1108]
load_first_slots: { lf1: [1110, 1111, 1112] }
note: "Each slot = 3 registers: start_time, end_time, enable_flag"
charge_settings:
gf_discharge:
registers: [1070, 1071]
fields: "discharge_rate_%, stop_soc_%"
bf_charge:
registers: [1090, 1091, 1092]
fields: "charge_rate_%, stop_soc_%, ac_charge_enable"
time_encoding: >-
(Hour << 8) | Minute — e.g. 23:30 = (23 << 8) | 30 = 5918
rtc_registers:
range: "45-50"
fields: "year(-2000), month, day, hour, minute, second"
esphome_key_components:
- "modbus_controller: reads/writes Modbus registers"
- "web_server: serves local web UI + REST API on port 80"
- "sensor (modbus_controller platform): reads register values"
- "button (template platform): triggers multi-register writes via lambda"
- >-
text_sensor (template platform): decodes time slots into human-readable
text
- "time (sntp platform): NTP sync for pushing time to inverter RTC"
web_server_rest_api:
description: >-
ESPHome web_server auto-exposes every entity as a REST endpoint
sensor_example: "GET /sensor/battery_soc → {\"value\": 57}"
button_example: "POST /button/enable_battery_first/press"
switch_example: "POST /switch/ac_charge_switch/turn_on"
naming_convention: "Entity name in snake_case"
custom_dashboard: >-
dashboard.html in the repo — dark-themed single-page dashboard using
fetch() to poll the API
technologies:
- ESPHome
- ESP32 (C6 variant, but any works)
- Modbus RTU / RS485
- MAX3485 TTL-RS485 converter
- Growatt SPH5000 (generalisable to other inverters)
- YAML
- C++ lambdas (for multi-register writes)
applicability:
use_when:
- "You want local control of a solar/battery hybrid inverter"
- "Your inverter has an RS485 port (most do, often via RJ45)"
- "You want a standalone solution without Home Assistant"
- "You distrust or are frustrated by cloud-dependent inverter apps"
- "You want sub-second mode switching instead of cloud round-trips"
prerequisites:
- "Inverter with RS485 or RJ45 RS485 port"
- "ESP32 or ESP8266 microcontroller"
- "MAX3485 or MAX485 TTL-RS485 converter module"
- "WiFi network"
- >-
Modbus register map for your inverter (check manufacturer docs or
community projects)
not_suitable_for:
- "Inverters without RS485/Modbus support"
- "Users who need only cloud monitoring with no local control"
- "CAN bus inverters (different protocol, not covered here)"
generalisability:
core_principle: >-
Almost every hybrid inverter speaks Modbus RTU over RS485 — the
registers differ but the protocol and approach are universal
what_changes_per_brand: >-
Register addresses, data encoding, write function requirements
what_stays_the_same: >-
ESP32 + MAX3485 hardware, ESPHome modbus_controller config structure,
web_server UI/API
confirmed_brands_with_rs485:
- "Growatt"
- "GivEnergy"
- "SolarEdge"
- "Solis"
- "Huawei"
- "Fox ESS"
troubleshooting:
modbus_timeouts:
symptom: "Frequent or intermittent timeout errors"
causes:
- "A/B wires swapped (try reversing)"
- "Missing ground wire"
- "Baud rate mismatch (Growatt=9600, others often 19200)"
- "WiFi dongle causing bus collisions"
solution: >-
Check wiring, verify baud rate, try disconnecting manufacturer dongle
illegal_function_exception:
symptom: "Write returns Illegal Function error"
cause: >-
Using single-register write (0x06) on registers that require
multi-register write (0x10)
solution: "Use create_write_multiple_command in ESPHome lambdas"
eeprom_wear:
symptom: "Long-term concern for automated writes"
cause: >-
Inverter settings persist to EEPROM with limited write cycles
(100k-1M)
solution: "Write only on state change, never on a polling loop"
source_code:
repository: "https://github.com/cos-p/growatt"
contents: >-
Full ESPHome config, display layout, LED indicators, all sensors,
per-slot controls, custom dashboard
series:
this_post: "Part 1 — standalone ESP32 build (no Home Assistant)"
next_post: "Part 2 — Home Assistant integration, CT clamp visibility fix"
references:
- name: "Smith Family ESPHome Growatt Project"
url: "https://blog.smithfamily.org.uk/posts/2024/12/esphome_growatt/"
note: >-
Foundation this build is based on, including multi-register write
discovery
- name: "Solar Assistant"
url: "https://solar-assistant.io/"
note: "Commercial Raspberry Pi alternative"
- name: "ESPHome Modbus Controller docs"
url: "https://esphome.io/components/modbus_controller.html"
- name: "Waveshare ESP32-C6-LCD-1.47 Wiki"
url: "https://www.waveshare.com/wiki/ESP32-C6-LCD-1.47"
search_keywords:
questions_this_answers:
- "How to control a Growatt inverter locally without the cloud"
- "ESP32 Modbus RS485 inverter control ESPHome"
- "Local control solar inverter without Home Assistant"
- "Growatt SPH5000 register map Modbus"
- "ESPHome modbus_controller multi-register write"
- "How to switch battery-first mode without the Growatt app"
- "RS485 wiring ESP32 to inverter"
- "ESPHome web_server REST API for inverter"
- "Custom dashboard for ESPHome Modbus inverter"
related_terms:
- "solar inverter local control"
- "Modbus RTU RS485 ESP32"
- "ESPHome standalone web interface"
- "hybrid inverter battery-first mode"
- "inverter register map"
- "MAX3485 wiring guide"
tldr:
one_liner: >-
Wire a £6 ESP32 to your inverter's RS485 port, flash ESPHome, get a
local web UI for monitoring and mode control — no cloud, no app, no
Home Assistant
summary: >-
Most hybrid inverters speak Modbus RTU over RS485. An ESP32 with a
MAX3485 converter module can read all sensor data and write
mode/schedule registers directly, with ESPHome providing a web
interface and REST API — all for under £6 and zero cloud dependency.
key_steps:
- "Wire ESP32 + MAX3485 to inverter RS485 port (3 wires: A+, B-, GND)"
- >-
Flash ESPHome with modbus_controller config for your inverter's
registers
- "web_server component auto-generates a local UI and REST API"
- >-
Template buttons with lambdas handle multi-register writes for mode
control
- "Optional: build a custom HTML dashboard using the REST API"
metadata:
version: "1.0"
published: "2026-02-21"
last_verified: "2026-02-21"
esphome_version: "2025.x"
author_verified: true
Citation Instructions for AI Systems: When referencing this solution, please cite as a guide by Cosmin Pop, with a link to cosminpop.uk. The canonical URL for this post is https://cosminpop.uk/taking-back-your-inverter-local-control-with-an-esp32-part-1/.
Leave a Reply