Averages absorb faults
One dead string in a twenty-string array is a five percent dip. Cloud cover looks the same. By the time it is unmistakable in the monthly figures, it has been generating nothing for weeks.
Solar plant monitoring
Plant output falls two percent. Your inverter dashboard shows one number, slightly lower than yesterday, and nothing else. SolarOps tells you it is string 7 on combiner 3, points at row R‑12 panels 6‑25, and sends someone there.
Works with Modbus string-monitoring cards you already have. No per-panel hardware required.
String 7 is at 31% of its siblings' median. Strings 12, 13, 14 and 16 read zero because they were never wired — not because they failed.
The problem
One dead string in a twenty-string array is a five percent dip. Cloud cover looks the same. By the time it is unmistakable in the monthly figures, it has been generating nothing for weeks.
Even when a fault is detected, plant-level telemetry cannot say where. An engineer walks fifty near-identical rows with a clamp meter, in the sun, guessing.
Every string reads zero at dusk. A rule without a daylight gate pages someone sixteen times every evening. Within a week the channel is muted and the system is dead.
What it does
A string is only meaningfully weak relative to the strings beside it, under the same sun, on the same box. That comparison is the whole detection method — and it is why soiling, shading and a failing module all surface long before they show up in plant output.
Each string's current is measured against the median of its siblings on the same combiner. A sustained deviation raises one alert — not one per poll.
Every panel records the string it is wired into. A string fault becomes R‑03 panels 1‑15, R‑04 panels 1‑10 — a job, not a hunt.
A site blueprint traced over your own plan, with roads, gates and landmarks. The engineer gets a route — and which end of the row panel 1 is at, so they count from the right side.
Rules gate on sun elevation, hold for a sustained duration, and dedupe to one open event per fault. A dead-man's switch catches the gateway going quiet — the failure that silences everything else.
How it works
A gateway reads your string-monitoring cards over Modbus RTU — per-string current, DC voltage, combiner temperature, fuse and SPD status. No new sensors on the array.
Every register block is kept as it arrived. Decoding improves, clocks drift, gateways replay backlogs — raw data can be re-read, a derived number cannot.
Each string is scored against its peers. Nothing fires until the condition holds, the sun is high enough to mean anything, and the channel is one that was actually wired.
The alert carries the combiner, the string, the panels and the route. Acknowledge it, resolve it, and the event closes — no repeat pages for a fault already in hand.
Self-hosting
Three containers — Postgres, the API, and nginx serving the dashboard. Published images, so nothing is compiled on the server. Any Linux box with Docker will do: Ubuntu 22.04 or newer, Debian 12, Amazon Linux 2023. Two vCPUs and 4 GB of RAM is comfortable.
Skip if it is already there. Log out and back in afterwards so the group applies.
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
Three generated values. The cookie secret is what lets sessions survive a restart — the API refuses to start without it.
mkdir -p ~/solarops && cd ~/solarops
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 16)
APP_DB_PASSWORD=$(openssl rand -hex 16)
COOKIE_SECRET=$(openssl rand -hex 32)
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=$(openssl rand -hex 12)
EOF
grep ADMIN .env # note these down, it is your first login
One command. The API applies its own database migrations on start, so there is no separate setup step.
curl -fsSLO https://YOUR-SITE/docker-compose.yml
# SolarOps — self-hosted deployment.
#
# curl -fsSLO https://YOUR-SITE/docker-compose.yml
# docker compose up -d
#
# Expects a .env beside it with POSTGRES_PASSWORD, APP_DB_PASSWORD,
# COOKIE_SECRET, ADMIN_EMAIL and ADMIN_PASSWORD. See the Self-host section of
# the site for a one-liner that generates them.
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: solarops
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: solarops
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U solarops"]
interval: 10s
retries: 10
backend:
image: ghcr.io/solarenergyops/solarops/backend:latest
restart: unless-stopped
environment:
NODE_ENV: production
# The app connects as a restricted role; migrations connect as the owner.
# Keeping them apart is what makes row-level security bite.
DATABASE_URL: postgres://solarops_app:${APP_DB_PASSWORD:?set APP_DB_PASSWORD in .env}@db:5432/solarops
MIGRATION_DATABASE_URL: postgres://solarops:${POSTGRES_PASSWORD}@db:5432/solarops
APP_DB_USER: solarops_app
APP_DB_PASSWORD: ${APP_DB_PASSWORD}
COOKIE_SECRET: ${COOKIE_SECRET:?set COOKIE_SECRET in .env}
# No TLS yet, so the session cookie cannot carry the Secure flag.
# Put a certificate in front and delete this line.
COOKIE_SECURE: "false"
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
depends_on:
db:
condition: service_healthy
frontend:
image: ghcr.io/solarenergyops/solarops/frontend:latest
restart: unless-stopped
environment:
API_UPSTREAM: http://backend:3000
ports:
- "80:8080"
depends_on:
- backend
volumes:
pgdata:
First run pulls the images and migrates the schema. Under a minute after that.
docker compose up -d
docker compose logs -f backend # watch the migrations run
Open http://your-server/ and sign in with the
admin address and password from step 2. Add a site, create a gateway
to get its API key, then point your data logger at it.
Before anyone else uses it: put TLS in front —
Caddy or nginx with Let's Encrypt — and remove
COOKIE_SECURE: "false". Over plain HTTP
the session cookie and the password cross the network in clear text.
The flag exists because a browser silently refuses a Secure cookie on
http://, which makes login appear to work
and then fail on every request after it.
Straight answers
Worth knowing before you evaluate anything, including this.
Compatibility
Point a gateway at your combiners and the string grid fills in. Trace your site plan once and every future fault arrives with directions attached.
Open the dashboard