A self-hosted solar inverter monitoring system. Polls inverters over the Solarman Modbus RTU protocol, stores time-series data in TimescaleDB, and streams live updates to a React dashboard over WebSockets.
Built for personal use across a small fleet of inverters (Deye and DAH Solar), with register maps defined per-model so new inverter types can be added without touching backend code.
Deye SUN-6K-SG05LP1-EU
DAH Solar DHN-LVEH3P12K-EU-G1
Deye SUN-5K-G05P1-EU-AM2
- Polls multiple inverters in parallel via a management command
- Register mapping is fully config-driven (YAML) — add a new inverter model without touching backend code
- Live dashboard over WebSockets (Django Channels) plus REST endpoints for history and stats
- Time-series storage on TimescaleDB (Postgres) with hypertables and compression
- JWT authentication (
djangorestframework-simplejwt) - Per-inverter daily/weekly energy stats (production, consumption, import, export)
Backend: Django 6, Django REST Framework, Django Channels + Daphne (ASGI), Redis, TimescaleDB (Postgres), pysolarmanv5
Frontend: React 19, Vite, Tailwind CSS, Recharts, React Router, Axios
Infra: Docker Compose (db, redis, backend, poller, frontend)
.
├── backend/
│ ├── core/ # Django project settings, ASGI/WSGI entrypoints
│ ├── monitoring/
│ │ ├── configs/ # YAML register maps, one per inverter model
│ │ ├── inverter_reader.py # Generic Modbus reader driven by YAML config
│ │ ├── management/commands/run_polling.py # Polling loop (parallel, threaded)
│ │ ├── models.py # Inverter, InverterData, EnergyStats
│ │ ├── consumers.py # WebSocket consumer
│ │ └── views.py / urls.py / serializers.py
│ ├── manage.py
│ ├── requirements.txt
│ └── .env # not committed — see Environment variables
├── frontend/ # React + Vite dashboard
├── backend.Dockerfile
├── frontend.Dockerfile
└── docker-compose.yml # db, redis, backend, poller, frontend
Register maps live in backend/monitoring/configs/*.yaml. Each file defines:
register_blocks— which Modbus register ranges to pollregisters— named metrics mapped to an address, type (uint16/int16/uint32/int32), and scalepv_strings— per-string voltage/current addressespv_total_power_sources/grid_total_power_sources— registers to sum for aggregate valuesdatabase_mapping— which parsed metrics get written toInverterDatamodel fields
To add a new inverter model, copy an existing config, adjust the register addresses for your model, and create an Inverter row in the admin pointing config_name at the new file (without the .yaml extension). No code changes required unless the model needs a metric the reader doesn't already support.
Register maps in this repo were reverse-engineered by hand against live hardware (no official Modbus documentation was available for these models), so they may not be 100% complete or accurate for every firmware revision.
- Docker and Docker Compose
cp backend/.env.example backend/.env # fill in secrets, DB credentials, etc.docker compose up --buildThis starts, in order:
db— TimescaleDB, onlocalhost:5433redis— onlocalhost:6379backend— runs migrations, then serves the API over Daphne (ASGI) onlocalhost:8001poller— runsmanage.py run_pollingin a loop, polling all active invertersfrontend— Vite dev server onlocalhost:5173, proxying/apiand/wstobackend
Open http://localhost:5173 for the dashboard.
Create a superuser and register your inverters through the Django admin:
docker compose exec backend python manage.py createsuperuserThen visit http://localhost:8001/admin/ and add an Inverter entry per device (IP, port, Modbus ID, and config_name pointing at one of the YAML files in backend/monitoring/configs/).
docker compose logs -f backend
docker compose logs -f poller| Endpoint | Description |
|---|---|
POST /api/token/ |
Obtain JWT access/refresh token pair |
POST /api/token/refresh/ |
Refresh access token |
GET /api/inverters/ |
List inverters |
GET /api/inverters/{id}/realtime/ |
Latest reading for an inverter |
GET /api/inverters/{id}/history/?hours=N |
Historical readings for the last N hours |
GET /api/inverters/{id}/stats/?days=N |
Daily energy stats for the last N days |
WS /ws/inverter/{id}/ |
Live push of new readings |
See backend/.env.example. At minimum you'll need:
DJANGO_SECRET_KEY,DJANGO_DEBUG,DJANGO_ALLOWED_HOSTSDB_NAME,DB_USER,DB_PASSWORD,DB_HOST,DB_PORTREDIS_HOST,REDIS_PORTCORS_ALLOWED_ORIGINS
docker-compose.yml overrides DB_HOST/REDIS_HOST to the in-network service names (db, redis) automatically — the values in .env are only used for running services outside Docker.
MIT — see LICENSE.
The Solarman Modbus register maps used here were derived experimentally and are not affiliated with or endorsed by Deye, DAH Solar, or Solarman. Use at your own risk, particularly if writing to holding registers.


