How HOP Sensors connects substation SCADA systems to the cloud, detects anomalies before they become outages, and delivers real-time operational intelligence to grid operators.
HOP Sensors is a cloud-native SCADA observability platform built for electric utilities. Substation systems transmit data over HTTPS to a REST API. A machine learning engine scores every substation for anomalies every 60 seconds. Operators view results in a live web dashboard refreshing every 5 seconds. Automated email alerts notify staff of feeder trips and abnormal conditions — no on-premises hardware required.
| Capability | Details |
|---|---|
| Data transmission | HTTPS REST API with pre-shared API key authentication |
| Alert delivery | Email — anomaly alerts, feeder trips, and restore confirmations |
| User access model | Role-based, per-utility, per-substation access control |
| Platform generation | v5 — React dashboard, replaced Grafana-based v4 |
The v5 platform is a six-service architecture running on DigitalOcean cloud infrastructure. Each service is containerized with Docker and orchestrated via Docker Compose, split between two servers: a SCADA droplet running all data pipeline services, and a mail droplet handling the public website and email delivery.
Substation SCADA Device (RTU / IED / DMS)
|
| HTTPS + API Key
v
FastAPI Ingest API --------> InfluxDB 3 Core
|
ML Service (z-score, every 60s)
|
Alerting Service -------> Email (SendGrid)
React Dashboard <-------- FastAPI Query API <-------- InfluxDB 3
portal.hopsensorsllc.com -- refreshes every 5s
REST API handling ingest from substations and query requests from the dashboard. Authenticates all requests via X-API-Key header.
Purpose-built time-series database storing telemetry, events, outages, and ML scores. Queried via SQL.
Z-score anomaly detection on frequency readings every 60 seconds. Dynamically discovers substations and writes scores back through FastAPI.
Polls for anomalies and feeder events. Per-source cooldowns and global rate limits prevent alert fatigue. Delivers via mail droplet API.
Single-page React + Vite app served by nginx. Six tabs: Telemetry, Alarms, Frequency, ML Anomaly, Executive View, Maps. Refreshes every 5s.
Flask app providing login, session management, user administration, and per-user substation access control. Backed by SQLite.
Substation systems transmit data over HTTPS using a pre-shared API key. Three endpoints handle the three data types the platform tracks.
| Endpoint | Data Type | Key Fields |
|---|---|---|
| POST /ingest/telemetry | Real-time readings per feeder/substation | voltage_kv, current_a, power_mw, frequency_hz |
| POST /ingest/event | Discrete grid events (feeder trips, restores) | device_id, feeder_id, severity, event_type, old_state, new_state |
| POST /ingest/outage | Outage records with customer impact | outage_id, feeder_id, customers_out, mw_interrupted, status, cause |
// POST /ingest/telemetry -- Header: X-API-Key: <key> { "substation_id": "SUB001", "feeder_id": "FDR002", "device_id": "SUB001-FDR002", "site_name": "Portland North", "latitude": 45.5231, "longitude": -122.6765, "voltage_kv": 229.8, "current_a": 1187.4, "power_mw": 274.2, "frequency_hz": 60.002 }
Integration Note: Any system capable of HTTPS POST requests can integrate — existing SCADA platforms, DMS, RTU data collectors, or custom middleware. No proprietary hardware or protocol adapters required.
HOP Sensors v5 uses InfluxDB 3 Core, a purpose-built time-series database optimized for high-frequency, append-only measurement data. The platform queries via SQL, replacing the Flux query language used in v4.
| Measurement | Tags (indexed) | Fields |
|---|---|---|
| scada_grid | substation_id, feeder_id, device_id, site_name | voltage_kv, current_a, power_mw, frequency_hz, latitude, longitude, anomaly_score, is_anomaly, z_score, baseline_mean, baseline_std, latest_value, model_version |
| scada_events | device_id, feeder_id, severity, event_type | old_state, new_state, message, latitude, longitude |
| outages | outage_id, feeder_id, status, region, cause | customers_out, mw_interrupted, etr_time, latitude, longitude |
v4 to v5: Migrated from Grafana + Flux queries to a custom React dashboard over SQL through FastAPI — replacing proprietary Grafana config with fully version-controlled application code.
The ML service runs a statistical z-score anomaly detector on grid frequency. Every 60 seconds it fetches the most recent 20 readings per substation, computes a rolling baseline, and scores the latest reading. No training data or GPU resources required — fully interpretable and auditable.
# For each substation -- last N=20 frequency readings: mean = mean(readings) std = stddev(readings) z = (latest - mean) / std # standard deviations from baseline score = min(abs(z) / Z_THRESHOLD, 1.0) # normalized to [0.0, 1.0] # Classification: NORMAL : score < 0.4 # z < 1.0 sigma ELEVATED : score < 0.7 # 1.0 sigma to 1.75 sigma ANOMALY : score >= 0.7 # z >= 1.75 sigma -- triggers alert email
| Field Written to DB | Description |
|---|---|
| anomaly_score | Normalized 0.0–1.0. Higher = more anomalous. |
| is_anomaly | Boolean. True when |z| >= Z_THRESHOLD (default 2.5 sigma). |
| z_score | Raw z-score in standard deviations from baseline mean. |
| baseline_mean | Mean of the scoring window (rolling average). |
| baseline_std | Standard deviation of the scoring window. |
| latest_value | Most recent frequency reading scored. |
| model_version | Model identifier for auditability (e.g. zscore-v1). |
The alerting service polls InfluxDB and the event API every 60 seconds. Three alert types with independent rate-limiting logic prevent alert fatigue. Delivery is via SendGrid SMTP through the mail droplet.
| Alert Type | Trigger | Per-Source Cooldown | Global Cap |
|---|---|---|---|
| ANOMALY ALERT | anomaly_score >= 0.7 on any substation | 1 hour per substation | 2 / hour |
| FEEDER TRIP | FEEDER_TRIP event with severity=ALARM | 30 min per feeder | 3 / hour |
| FEEDER RESTORED | FEEDER_RESTORE, only if a TRIP alert was sent for that feeder | None | Shared with TRIP |
The SCADA droplet authenticates to the mail droplet's API with an X-Alert-Token shared secret. Email content includes substation ID, detected values, z-score, baseline context, and a direct link to the portal dashboard.
Design note: Alerting is fully decoupled from the dashboard. Operators receive email alerts even when not logged in, and delivery does not depend on any browser session.
The dashboard is a React single-page application at portal.hopsensorsllc.com. It auto-refreshes every 5 seconds and requires only a modern web browser — no plugins, extensions, or client software.
| Tab | Purpose | Key Components |
|---|---|---|
| Telemetry | Live readings per substation | Live panel (anomaly score, voltage, current, power, frequency), time-series charts |
| Alarms & Outages | Operational event feed | Color-coded event log, active outage table with MW and customer impact |
| Frequency | Grid frequency vs. NERC bounds | Per-substation dials (59.85–60.15 Hz normal), multi-substation threshold plot |
| ML Anomaly | Anomaly scores and trend | Score cards (Normal / Elevated / Anomaly), trend scatter plot, z-score detail table |
| Executive View | Single-screen grid health | Grid health %, active outages, MW interrupted, anomalies detected, per-substation status |
| Maps | Geographic visualization | Leaflet map, substation markers, outage polygon overlays, GIS data input |
Features: light/dark mode, time range selector (5m / 15m / 30m / 1h / 6h / 24h / 7d), role-based default tab (executives land on Executive View, operators on Telemetry).
Authentication is handled by a Flask app backed by SQLite. Users log in at portal.hopsensorsllc.com/login with work email and password. Sessions are server-side cookies; passwords are bcrypt hashed.
| Field | Description |
|---|---|
| Unique work email. Used as login username. | |
| name | Display name shown in the dashboard header. |
| org_name | Utility organization (e.g. Pacific Power Co.) |
| role | operator or admin. Admins access the /admin user management panel. |
| active | Boolean. Deactivated users cannot log in; records are retained. |
| substations | List of substation IDs the user is authorized to view. |
Per-user substation filtering is enforced in the React frontend via session data injected at login. Admins manage users at /admin without SSH access.
The platform runs on two DigitalOcean Droplets. All v5 services run as Docker containers with restart: unless-stopped, orchestrated by Docker Compose.
| Server | Hostname | Services |
|---|---|---|
| SCADA Droplet | hopsensors-scada | v5-fastapi, v5-influxdb, v5-ml, v5-alerting, v5-auth, v5-frontend, v5-simulator |
| Mail Droplet | mail (do-linpaws) | hopsensors-api (systemd), nginx, Postfix, Dovecot |
Public DNS points hopsensorsllc.com and portal.hopsensorsllc.com to the mail droplet's nginx. The portal subdomain is reverse-proxied to the auth service on the SCADA droplet. All containers share the v5-hopsensors-net Docker bridge network. InfluxDB (8181) and FastAPI (8000) are not exposed to the public internet.
| Control | Implementation |
|---|---|
| Transport encryption | All public endpoints over HTTPS/TLS via nginx. Internal container traffic on isolated Docker bridge network. |
| Ingest API auth | Every request requires X-API-Key header, managed via environment variables. |
| Alert channel auth | X-Alert-Token shared secret between SCADA and mail droplets. Channel not publicly exposed. |
| Portal auth | Session-based login. Bcrypt hashed passwords. Inactive users hard-blocked. |
| Brute-force protection | fail2ban on both droplets monitoring SSH and nginx access logs. |
| Attack surface reduction | InfluxDB and FastAPI not publicly exposed. Legacy Grafana disabled and removed. |
| Data isolation | Per-user substation access enforced. Users see only their assigned substations. |
Roadmap: SOC 2 Type I audit logging planned for a future release, targeting enterprise utility customers with formal compliance requirements.
All endpoints require X-API-Key header. Base URL: https://portal.hopsensorsllc.com/api
| Method + Path | Description |
|---|---|
| POST /ingest/telemetry | Write a telemetry reading for a substation/feeder |
| POST /ingest/event | Write a discrete grid event (trip, restore, etc.) |
| POST /ingest/outage | Write or update an outage record |
| Method + Path | Description |
|---|---|
| GET /query/telemetry/latest | Latest reading per substation (last 1 minute) |
| GET /query/telemetry/timeseries | Voltage/power/frequency time series — params: substation_id, minutes |
| GET /query/ml/latest | Latest ML anomaly score per substation |
| GET /query/ml/trend | Anomaly score trend for all substations — param: minutes |
| GET /query/events | Recent events/alarms — param: minutes |
| GET /query/outages/active | Currently active outages |
| GET /query/frequency/thresholds | Frequency readings for threshold plot — param: minutes |
| GET /query/substations | Discover all active substation IDs and coordinates |
| GET /health | API health check |