A Prometheus exporter for CyberPower UPS systems β reads pwrstatd over a Unix socket and exposes battery, load, and power metrics on port 9200.
π Quick Start | π Metrics | βοΈ Configuration | π¨βπ» Development | π Attribution
A single-purpose Prometheus exporter that talks to the CyberPower PowerPanel pwrstatd daemon over its Unix socket and translates the response into Prometheus metrics. It is shipped as a multi-arch (linux/amd64, linux/arm64) Docker image to GitHub Container Registry.
Drop it next to your UPS host, scrape :9200/metrics, and you get battery health, line voltage, load, and event signals without touching SNMP or the proprietary GUI.
graph LR
UPS[("π CyberPower UPS")]
PWRSTATD[["βοΈ pwrstatd"]]
SOCK{{"π‘ /var/pwrstatd.ipc"}}
EXP[["π cyberpower-exporter"]]
PROM[("π₯ Prometheus")]
GRAF[["π Grafana / Alertmanager"]]
UPS -->|USB| PWRSTATD
PWRSTATD --- SOCK
SOCK -->|STATUS command| EXP
EXP -->|HTTP :9200/metrics| PROM
PROM --> GRAF
style UPS fill:#e1f5fe,stroke:#01579b,stroke-width:2px
style PWRSTATD fill:#fff3e0,stroke:#e65100,stroke-width:2px
style SOCK fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
style EXP fill:#e0f2f1,stroke:#004d40,stroke-width:2px
style PROM fill:#ffebee,stroke:#b71c1c,stroke-width:2px
style GRAF fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
The exporter polls pwrstatd every POLL_INTERVAL seconds, parses the key=value reply, and updates 11 Prometheus collectors plus an Info metric. The container runs as a non-root user added to the root group so it can read the host-owned pwrstatd.ipc socket.
- π Multi-arch image:
linux/amd64+linux/arm64, published toghcr.io/simplicityguy/cyberpower-exporter - π Hardened container: non-root user, healthcheck on
/metrics, ready for--cap-drop=ALL --security-opt=no-new-privileges:true - π¦ Reproducible builds: uv-based multi-stage Dockerfile with a committed
uv.lock - π Type-safe: full type hints, strict mypy validation, Bandit security scanning
- πͺ΅ Structured logging: structlog with JSON output to stdout, ISO-8601 UTC timestamps, contextvars-merged service tag, and structured exception tracebacks
- β Tested: 13-test pytest suite with mocked Unix socket and isolated Prometheus registry
- π€ CI/CD: code-quality + docker-validate + multi-arch build, monthly image cleanup, weekly scheduled rebuilds, Dependabot for actions, docker, and pip ecosystems
- π·οΈ OCI labels: full
org.opencontainers.image.*set populated from build args (date, version, revision, source, license, base image)
docker run -d \
--name cyberpower-exporter \
--restart unless-stopped \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
-v /var/pwrstatd.ipc:/var/pwrstatd.ipc \
-p 9200:9200 \
ghcr.io/simplicityguy/cyberpower-exporter:latest
# Scrape it
curl http://localhost:9200/metricsThe host must be running pwrstatd (CyberPower PowerPanel for Linux). The daemon's Unix socket at /var/pwrstatd.ipc must be mounted into the container.
scrape_configs:
- job_name: cyberpower
static_configs:
- targets: ["ups-host:9200"]| Metric | Type | Description |
|---|---|---|
ups_cyberpower_info |
Info | Model name and firmware number |
ups_utility_volt |
Gauge | Utility input voltage |
ups_output_volt |
Gauge | UPS output voltage |
ups_load |
Gauge | Load percentage (0-1) |
ups_battery_capacity |
Gauge | Battery capacity percentage |
ups_battery_remaining_time |
Gauge | Battery time remaining (seconds) |
ups_battery_charging |
Gauge | Battery charging (0/1) |
ups_battery_discharging |
Gauge | Battery discharging (0/1) |
ups_ac_present |
Gauge | AC power present (0/1) |
ups_diagnostic_result |
Gauge | Last diagnostic result |
ups_input_rating_volt |
Gauge | Input voltage rating |
ups_output_rating_watt |
Gauge | Output wattage rating |
| Environment Variable | Default | Description |
|---|---|---|
POLL_INTERVAL |
5 |
Seconds between UPS status polls |
LOG_LEVEL |
INFO |
Log level β DEBUG, INFO, WARNING, ERROR, CRITICAL |
LISTEN_ADDRESS |
0.0.0.0:9200 |
Bind address (set by the image) |
The container runs as a non-root exporter user in the root group so it can read the host-owned socket. The exporter binds 0.0.0.0:9200 so it is reachable from outside the container.
Logs are emitted as one JSON object per line on stdout, ready for ingestion by Loki, Fluent Bit, Vector, or Docker's json-file driver:
{"event": "exporter started", "listen_port": 9200, "poll_interval": 5, "socket": "/var/pwrstatd.ipc", "service": "cyberpower-exporter", "level": "info", "logger": "cyberpower_exporter.exporter", "timestamp": "2026-05-01T18:11:35.735897Z"}This project mirrors the SimplicityGuy/discogsography toolchain and uses uv for Python dependency management.
git clone https://github.com/SimplicityGuy/cyberpower-exporter.git
cd cyberpower-exporter
uv sync --all-groups # Install runtime + dev deps into .venv
uv run pre-commit install # Install git hooksuv run cyberpower-exporter # Run the exporter (entry point)
uv run pytest # Run the test suite (13 tests)
uv run pytest --cov=cyberpower_exporter # With coverage report
uv run ruff check . # Lint
uv run ruff format . # Format
uv run mypy . # Strict type checking
uv run bandit -c pyproject.toml -r src/ # Security scan
uv run pre-commit run --all-files # Full quality gate
docker build -t cyberpower-exporter:dev . # Local image buildsrc/cyberpower_exporter/ # Package: __init__.py, exporter.py, py.typed
tests/ # pytest suite with mocked Unix socket
.github/actions/ # setup-python-uv composite action
.github/workflows/ # build.yml, cleanup-cache.yml, cleanup-images.yml
.github/dependabot.yml # github-actions, docker, pip ecosystems
pyproject.toml # ruff/mypy/bandit/pytest config + uv environments
uv.lock # Pinned dependencies (committed)
Dockerfile # uv-based multi-stage build
| Tool | Purpose |
|---|---|
| ruff | Lint + format (configured to match upstream) |
| mypy | Strict type checking with disallow_untyped_defs, warn_unreachable, strict_equality |
| bandit | Security scanning (B104 skipped β see pyproject.toml) |
| pytest | Unit tests with unittest.mock for the Unix socket and a registry-isolation fixture |
| hadolint | Dockerfile linting via hadolint/hadolint-action (failure-threshold: error) |
| shellcheck | Shell script linting |
| shfmt | Shell script formatting |
| actionlint | GitHub Actions workflow linting |
| yamllint | YAML linting (config in .yamllint) |
See CLAUDE.md for the full development guide and AI-assistant rules.
This project is derived from Mike Shoup's shouptech/cyberpower_exporter β specifically src/cyberpower_exporter/command.py. The original work is licensed under Apache 2.0; this fork preserves that license and the original copyright notice. See LICENSE and NOTICE for details.
- π Bug Reports: GitHub Issues
- π‘ Feature Requests: GitHub Discussions
- π¬ Questions: Discussions Q&A
- shouptech/cyberpower_exporter β Mike Shoup's original (this project's upstream)
- dgosxha/cyberpower-pdu-exporter β for CyberPower PDUs over SNMP
- networkupstools/nut β generic NUT-based UPS support with its own Prometheus exporter
Licensed under the Apache License, Version 2.0 β see LICENSE for the full text and NOTICE for upstream attribution required by Β§4(d).
- π Mike Shoup for the original
cyberpower_exporterthis project derives from - π₯ Prometheus and the
prometheus_clientmaintainers - β‘ CyberPower Systems for
pwrstatdand PowerPanel for Linux - π uv for blazing-fast package management
- π₯ Ruff for lightning-fast linting
- π The Python community for excellent libraries and tools