Skip to content

zavan/ghrian-agent

Repository files navigation

ghrian-agent

Real-time solar inverter data collector: Modbus TCP → MQTT.

Written in Go. Uses device register maps (JSON) for different inverter models. Focuses on live power flow (PV, grid, battery, load) + model info by default.

Features

  • Modbus TCP (simonvetter/modbus) with mandatory range merging for minimal reads (protects the inverter from too many small requests).
  • Modern MQTT v5 client (eclipse-paho/paho.golang + autopaho) with auto-reconnect and Last Will (availability).
  • Curated defaults for "live" data only (no flood of 60+ fault bits unless you ask via --poll).
  • Rich JSON payload with value, unit, label for each metric.
  • Config file (YAML/JSON) + env + CLI (CLI wins).
  • Stdout poll summary logs every cycle.
  • Self-contained (devices embedded).
  • Graceful shutdown + reconnects.

Quick start

# build the binary (standard Go layout)
go build -o ghrian-agent ./cmd/agent

# help
./ghrian-agent --help

# example (uses dev mqtt on localhost:1883)
./ghrian-agent \
  --device-model solis_s5-eh1p5k-l \
  --modbus 192.168.1.50 \
  --mqtt-broker tcp://localhost:1883 \
  --mqtt-topic ghrian/inverter/01

With the dev compose (from this repo):

make dev-up
# in another terminal:
make dev-sub   # easiest (execs into container, uses localhost)
# or manually: docker compose -f dev/compose.yml -p agent up -d --wait
# run agent (point modbus at your inverter or a simulator)

Docker

Published to Docker Hub as felipezavan/ghrian-agent (multi-arch: amd64 + arm64). It's a tiny static image with no config file baked in — configure it entirely with flags or env vars:

docker run --rm felipezavan/ghrian-agent \
  --device-model solis_s5-eh1p5k-l \
  --modbus 192.168.1.50:502 \
  --mqtt-broker tcp://broker:1883 \
  --mqtt-topic ghrian/inverter/01

# equivalently, via env vars:
docker run --rm \
  -e DEVICE_MODEL=solis_s5-eh1p5k-l -e MODBUS_ADDR=192.168.1.50:502 \
  -e MQTT_BROKER=tcp://broker:1883 -e MQTT_TOPIC=ghrian/inverter/01 \
  felipezavan/ghrian-agent

Tags: X.Y.Z / latest from releases, edge tracks main.

Run on a Raspberry Pi (Docker Compose)

The agent is the one piece that has to sit on your LAN next to the inverter, and a Raspberry Pi is a natural home for it. The image is multi-arch, so the arm64 variant is pulled automatically — nothing special to do. This repo ships a standalone compose.yml that runs only the agent (your broker and server live elsewhere) and restarts it on boot:

git clone https://github.com/zavan/ghrian-agent
cd ghrian-agent
cp .env.example .env     # set MODBUS_ADDR + MQTT_BROKER (see the comments)
docker compose up -d     # pulls the arm64 image and starts polling

That's the whole deploy. From then on:

docker compose logs -f                          # watch the per-cycle poll summary
docker compose pull && docker compose up -d     # update to a newer image
docker compose down                             # stop it

Don't want a git checkout on the Pi? The two files are all you need — copy compose.yml and .env.example onto the Pi, rename the latter to .env, and run docker compose up -d in that directory.

To run the whole stack (broker + server + agent) on a single host instead, use the compose.yml in the top-level ghrian repo — this one is deliberately agent-only.

CLI + Config

All params can come from:

  1. --config path/to/config.yaml (or auto-discovered: ./config.yaml, ~/.config/ghrian-agent/config.yaml, /etc/...)
  2. Environment variables (MODBUS_ADDR, MQTT_BROKER, POLL=..., etc.)
  3. CLI flags (highest precedence)

Example config.yaml:

device_model: solis_s5-eh1p5k-l
modbus: 192.168.1.50:502
modbus_unit: 1
interval: 10
# poll: []   # omit or empty => curated live power flow + model defaults
mqtt:
  broker: tcp://localhost:1883
  topic: ghrian/inverter/01
  user: ""
  pass: ""
  qos: 1          # at-least-once delivery (drop to 0 for fire-and-forget on a trusted LAN)
  retain: true    # keep the last reading on the broker for fresh consumers

Key flags (see --help):

  • --device-model (required, matches internal/agent/devices/.json)
  • --modbus ip[:port] (default port 502)
  • --modbus-unit (default 1)
  • --interval (default 10)
  • --poll name1,name2,... (comma list; overrides defaults)
  • --mqtt-broker, --mqtt-topic, --mqtt-user, --mqtt-pass
  • --config

Secrets: prefer the MQTT_PASS env var or a config file for the broker password. A --mqtt-pass flag is visible to any user via ps/process listing and may land in shell history.

Payload

Each poll publishes a JSON message to --mqtt-topic where every metric is a self-describing { value, unit, label } object (bitfields also emit <name>_active: []string), plus a retained <topic>/availability (online/ offline). The agent sets online on connect, flips to offline after a few consecutive poll failures or on graceful shutdown (a clean MQTT DISCONNECT suppresses the Last Will, so the agent publishes it itself), and registers offline as its LWT for crash/network-drop. Units and labels come straight from the device JSON.

The full message format — topics, QoS/retain, bitfields, cumulative today_* counters, and availability semantics — is the project-wide contract every module agrees on:

ghrian/docs/payload.md

Default registers (live power flow + model)

See device.go:defaultLivePollNames. Includes PV power, AC power, battery V/I/SOC/power, load powers, grid port power, voltages/currents, frequency, temp, today's energies, status, model/serial.

Use --poll (or config) to request more (e.g. specific faults) or a subset.

Adding a new device model

  1. Drop your JSON file into internal/agent/devices/your-model.json (follow the structure of the Solis one; register_start/end, function_code 0x02/0x04, data_type (sign is encoded here — S16/S32 = signed), scale, unit, label, bit_definitions/enum_values as needed). If the model has LV/HV-split bitfields, add an lv_hv block (model_register, battery_model_register, lv_low_bytes, hv_low_bytes) so the LV/HV rule stays out of Go. The embed directive will pick it up.
  2. Optionally update the curated default list in device.go if you want it included for new models.
  3. Update README if publishing the model.
  4. The JSON was generated from the accompanying PDF protocol doc.

Development & testing

  • go test ./... (covers load, compute with sample raws, range merge, config precedence).
  • Mock device: the tests + poller_test.go (add your own) use the modbus server from the library to simulate an inverter serving the live registers.
  • MQTT dev broker + monitoring: make dev-up then make dev-sub (recommended — pins project name to match your "agent" UI/workspace project, waits for healthy via healthcheck, uses exec + localhost inside the container; no local client needed). Or manually (be consistent): docker compose -f dev/compose.yml -p agent up -d --wait then docker compose -f dev/compose.yml -p agent exec -T mosquitto mosquitto_sub -h localhost -p 1883 -t 'ghrian/#' -v (or plain mosquitto_sub if you have the CLI installed locally).
  • To test range merging / device friendliness: the merge logic is exercised in tests; it groups per FC and spans min-to-max (gaps filled) into as few <=50-register reads as possible (per PDF inter-frame guidance of max 50 regs / 100 bytes). E.g. sparse 33001, 33010, 33015 → single block read 33001-33015.
  • Poll logging goes to stdout on every cycle (visible when running the binary).

License

This project is licensed under the MIT License - see the LICENSE file for details.

Notes

Greenfield project for ghrian. Read-only. Use at your own risk with real inverters (start with low interval, monitor).

Cross-reference the PDF in internal/agent/devices/ (or the original source) for exact semantics, appendices (bitfields, models, etc.).

Pull requests welcome for more models / features.

About

Real-time solar inverter data collector: Modbus TCP → MQTT. Tiny static Go binary, gentle on the inverter.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors