Skip to content

Latest commit

Β 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

cyberpower-exporter

Build License: Apache 2.0 Python 3.13+ uv Ruff pre-commit mypy Bandit Docker Prometheus Claude Code

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

🎯 What is cyberpower-exporter?

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.

πŸ›οΈ Architecture Overview

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
Loading

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.

🌟 Key Features

  • πŸ‹ Multi-arch image: linux/amd64 + linux/arm64, published to ghcr.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)

πŸš€ Quick Start

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/metrics

The host must be running pwrstatd (CyberPower PowerPanel for Linux). The daemon's Unix socket at /var/pwrstatd.ipc must be mounted into the container.

Prometheus scrape config

scrape_configs:
  - job_name: cyberpower
    static_configs:
      - targets: ["ups-host:9200"]

πŸ“Š Exported Metrics

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

βš™οΈ Configuration

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.

Log format

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"}

πŸ‘¨β€πŸ’» Development

This project mirrors the SimplicityGuy/discogsography toolchain and uses uv for Python dependency management.

Setup

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 hooks

Common commands

uv 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 build

Repository layout

src/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

Quality standards

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.

πŸ“œ Attribution

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.

πŸ’¬ Support & Community

πŸ”— Other CyberPower / UPS Exporters

πŸ“„ License

Licensed under the Apache License, Version 2.0 β€” see LICENSE for the full text and NOTICE for upstream attribution required by Β§4(d).

πŸ™ Acknowledgments


Made with ❀️ in the Pacific Northwest

About

πŸ“¦ Dockerized CyberPower Exporter

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Packages

Used by

Contributors

Languages