This ESP tool reads the metrics data from the Solax X3 Hybrid Gen4 inverter via modbus and writes it to an InfluxDB.
- Reads the important metrics data from the Solax inverter modbus input register
- Stores the metrics in an InfluxDB (in my case hosted on a Raspberry Pi, visualized in a Grafana dashboard)
- Exposes the same data read-only in the local network as JSON (
GET /status) plus a health endpoint (GET /health) - Pushes the current grid feed-in surplus to a Nibe heat pump bridge
DATA_POLL_INTERVAL is 1000 ms, but each of the 42 input registers is read in its
own Modbus transaction with a busy-wait of at least 25 ms. The effective cycle
time is therefore roughly 2-3 seconds, not one second.
- The Solax X3 Hybrid Gen4 inverter provides a modbus RTU (RS485) interface (it is called COM on the inverter)
- Remark: some older versions of this inverter also supported modbus TCP but since the 4th generation this option was removed (I don't understand why??)
- The modbus default baudrate for this device is
19200(8N1, slave ID 1)
- I used the NodeMCU version of esp8266 (NodeMCU 1.0 / ESP-12E)
- A RJ45 socket is used for this interface
- For the RS485 connection on the ESP you need a level converter (in my case I used the MAX485)
On the inverter side — RJ45 pinout of the COM port:
| RJ45 pin | Signal |
|---|---|
| 4 | RS485 A |
| 5 | RS485 B |
| 6 | GND (optional, was not required in my case) |
On the ESP side — the MAX485 sits between them:
| MAX485 | NodeMCU | Direction |
|---|---|---|
RO (receiver out) |
D2 |
MAX485 -> ESP (RX) |
DI (driver in) |
D1 |
ESP -> MAX485 (TX) |
DE + RE (tied together) |
D3 |
ESP -> MAX485 (transmit enable) |
A / B |
— | to RJ45 pin 4 / 5 on the inverter |
Note that D1/D2 carry the TTL side (RO/DI), not A/B. A/B is the
RS485 differential pair going to the inverter.
Credentials are not in the repository. Copy the template and fill in your values:
cp project/secrets.h.example project/secrets.h
$EDITOR project/secrets.hproject/secrets.h is gitignored. It holds the WiFi credentials, the static IP,
the InfluxDB URL/token/org/bucket and the Nibe bridge URL. Set NIBE_API_URL to
"" if you don't have a heat pump to talk to.
Either open project/project.ino in the Arduino IDE as before, or use the
provided Makefile (requires arduino-cli):
make build # compile
make ports # find the board
make flash PORT=/dev/cu.usbserial-0001 # compile + upload
make monitor PORT=/dev/cu.usbserial-0001 # serial log at 115200 baudVerified toolchain (pinned in project/sketch.yaml):
| Component | Version |
|---|---|
| esp8266 core | 3.1.2 |
| modbus-esp8266 | 4.1.0 |
| ESP8266 Influxdb | 3.13.2 |
InfluxDB receives the raw register values, not scaled units. Most registers
carry a factor of 0.1 (voltage in 0.1 V, current in 0.1 A, energy in 0.1 kWh; the
feedin_energy_* / consum_energy_* counters use 0.01 kWh). The conversion happens
in the Grafana dashboard so that the historical series stay consistent. The factor
for every register is documented inline in project/project.ino.
| Endpoint | Response |
|---|---|
GET /status |
JSON object with the numeric registers from the most recent poll cycle, CORS enabled |
GET /health |
200 when modbus and InfluxDB are both fine, 503 with a JSON error body otherwise |
GET /nibe |
Current state of the heat pump push |
POST /nibe?enabled=true|false |
Switch the heat pump push on/off, persisted in EEPROM |
The push to the Nibe bridge can be toggled at runtime without reflashing. The
setting is stored in the emulated EEPROM and survives a reboot and a firmware
update. Accepted values are true|false, 1|0 and on|off.
curl http://192.168.1.41/nibe
# {"enabled":true,"configured":true,"active":true}
curl -X POST "http://192.168.1.41/nibe?enabled=false"
# {"enabled":false,"configured":true,"active":false}| Field | Meaning |
|---|---|
enabled |
the persisted switch |
configured |
whether NIBE_API_URL is set at all |
active |
enabled && configured — whether a push actually happens |
The default on a fresh chip is enabled: true, matching the behaviour before
the switch existed. The EEPROM is only written when the value actually changes,
so repeated calls cost no flash write cycles.