Solar plant monitoring

Find the fault.
Not just the drop.

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.

SCB‑03 · 16 strings string 7 low
low high spare

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

A bad panel hides inside a good-looking number

01

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.

02

"Somewhere in the array"

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.

03

Alerts that get muted

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

Every string, compared against its own neighbours

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.

Peer-median detection

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.

Panel-level localisation

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.

Walk-to-it directions

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.

Alerts worth keeping on

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

From RS485 to a name on a map

  1. 1

    Poll what is already there

    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.

  2. 2

    Store the raw frames

    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.

  3. 3

    Compare, then wait

    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.

  4. 4

    Send someone

    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

Running on your own machine in ten minutes

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.

  1. 1

    Install Docker

    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
  2. 2

    Write the secrets

    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
  3. 3

    Download the compose file

    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
    What is in it
    # 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:
  4. 4

    Start it

    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

What it does not do

Worth knowing before you evaluate anything, including this.

It does not measure individual panels.
A combiner card reports current per string. True per-panel readings need module-level electronics — optimisers, microinverters, or per-module monitors — which is a hardware purchase, not a software feature. SolarOps localises a fault to the panels on a string; it does not claim to read them.
Location is advisory, not survey-grade.
Phone GPS resolves to roughly five metres and rows sit about six metres apart. It gets you to the right area. The row label and the stated counting direction do the last ten metres.
Performance ratio needs more than combiners.
PR, CUF, specific yield and grid availability all need inverter, export meter or pyranometer feeds. Where those are absent, the figures are absent — not estimated and shown as though they were measured.

Compatibility

Built against real hardware

Combiner cards
Shunt-based PV string monitoring, 8–16 channels, up to 30 A per string, 1500 V DC
Field bus
Modbus RTU over RS485, 9600 8N1
Transport
MQTT to Azure IoT Hub, or direct HTTPS to the ingest API
Tenancy
Multi-tenant, isolated at the database with row-level security
Deployment
Docker images, Postgres, self-hosted or managed
Reads
String current, DC voltage and power, combiner temperature, fuse and isolator status

See it against your own plant

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