Docs: Getting Started | Configuration | API Reference | Architecture | MQTT Topics | SNMP OIDs | Multi-PDU | Security | Troubleshooting | Features
The bridge serves a REST API on port 8080 (configurable via BRIDGE_WEB_PORT). All endpoints return JSON unless otherwise noted. CORS is enabled for all origins.
When multiple PDUs are registered, most endpoints accept an optional ?device_id= query parameter to specify which PDU to target. If only one PDU is registered, the parameter is optional and the single PDU is auto-selected. If multiple PDUs are registered and no device_id is provided, the bridge defaults to the primary device or returns an error with a list of available devices.
# Single PDU (device_id optional)
curl http://localhost:8080/api/status
# Multi-PDU (specify which device)
curl http://localhost:8080/api/status?device_id=rack1-pduReturns the current state of the PDU: device info, ATS status, all bank metrics, all outlet states, and a summary.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
device_id |
No | Target PDU (required if multiple PDUs registered) |
Response (200):
{
"device": {
"name": "CyberPower PDU44001",
"id": "pdu44001",
"outlet_count": 10,
"phase_count": 1
},
"ats": {
"preferred_source": 1,
"preferred_label": "A",
"current_source": 1,
"current_label": "A",
"auto_transfer": true,
"transferred": false,
"redundancy_ok": true,
"source_a": {
"voltage": 121.3,
"frequency": 60.0,
"voltage_status": "normal"
},
"source_b": {
"voltage": 120.8,
"frequency": 60.0,
"voltage_status": "normal"
}
},
"inputs": {
"1": {
"number": 1,
"voltage": 121.3,
"current": 1.2,
"power": 145,
"apparent_power": 150,
"power_factor": 0.97,
"load_state": "normal"
}
},
"outlets": {
"1": {
"number": 1,
"name": "File Server",
"state": "on",
"current": 0.8,
"power": 95.0,
"energy": 12.5
}
},
"summary": {
"total_power": 145.0,
"input_voltage": 121.3,
"input_frequency": 60.0,
"active_outlets": 8,
"total_outlets": 10
},
"identity": {
"serial": "ABC123",
"model": "PDU44001",
"firmware_main": "01.03.01",
"outlet_count": 10,
"phase_count": 1
},
"mqtt": {
"connected": true,
"reconnect_count": 0,
"broker": "127.0.0.1",
"port": 1883
},
"data_age_seconds": 0.3,
"ts": 1708531200.0
}Error responses:
| Status | Condition |
|---|---|
| 400 | Multiple PDUs registered but no device_id provided |
| 503 | No data received from PDU yet |
Health check endpoint used by Docker HEALTHCHECK and external monitoring. Aggregates health across all registered PDUs.
Response (200 = healthy, 503 = degraded):
{
"status": "healthy",
"issues": [],
"pdu_count": 1,
"subsystems": {
"mqtt": {
"connected": true,
"reconnect_count": 0,
"broker": "127.0.0.1",
"port": 1883,
"publish_errors": 0,
"total_publishes": 4521
},
"history": {
"db_path": "/data/history.db",
"total_writes": 1500,
"write_errors": 0,
"retention_days": 60,
"healthy": true
}
},
"uptime_seconds": 3600.5
}When degraded, the issues array contains descriptive strings:
{
"status": "degraded",
"issues": [
"[rack1-pdu] Data is 45s stale",
"MQTT disconnected"
]
}These endpoints manage the multi-PDU configuration. See Multi-PDU for the full guide.
List all registered PDUs with their status.
Response (200):
{
"pdus": [
{
"device_id": "rack1-pdu",
"config": {
"device_id": "rack1-pdu",
"host": "192.168.20.177",
"snmp_port": 161,
"label": "Main Rack PDU",
"enabled": true
},
"identity": {
"serial": "ABC123",
"model": "PDU44001",
"firmware_main": "01.03.01"
},
"status": "healthy",
"data_age_seconds": 0.5,
"has_data": true
}
],
"count": 1
}Status values: healthy, degraded (data older than 30s), no_data, unknown.
Add a new PDU to the configuration.
Request body:
{
"device_id": "rack2-pdu",
"host": "192.168.20.178",
"snmp_port": 161,
"community_read": "public",
"community_write": "private",
"label": "Secondary Rack",
"enabled": true,
"num_banks": 2
}Response (201):
{
"device_id": "rack2-pdu",
"ok": true
}Error responses:
| Status | Condition |
|---|---|
| 400 | Missing device_id or host |
| 409 | PDU with that device_id already exists |
Update an existing PDU's configuration.
Request body: Same fields as POST.
Response (200):
{
"device_id": "rack2-pdu",
"ok": true
}Error: 404 if device_id not found.
Remove a PDU from the configuration.
Response (200):
{
"device_id": "rack2-pdu",
"deleted": true
}Error: 404 if device_id not found.
Trigger a network scan for CyberPower PDUs.
Response (200):
{
"discovered": [
{
"host": "192.168.20.177",
"model": "PDU44001",
"serial": "ABC123",
"name": "CyberPower PDU"
}
]
}Error: 503 if discovery callback is not configured.
Get the current bridge configuration.
Response (200):
{
"poll_interval": 1.0,
"port": 8080,
"pdu_count": 1,
"default_device_id": "pdu44001"
}Update bridge configuration at runtime (without restart).
Request body:
{
"poll_interval": 5.0
}Response (200):
{
"updated": {
"poll_interval": 5.0
},
"ok": true
}Constraints: poll_interval must be >= 1.
These endpoints write to the PDU hardware via SNMP SET.
Set the device name on the PDU via SNMP.
Query parameters: device_id (optional, required for multi-PDU)
Request body:
{
"name": "Main Rack PDU"
}Response (200):
{
"device_id": "pdu44001",
"name": "Main Rack PDU",
"ok": true
}Set the sysLocation field on the PDU via SNMP.
Query parameters: device_id (optional, required for multi-PDU)
Request body:
{
"location": "Server Room A, Rack 3"
}Response (200):
{
"device_id": "pdu44001",
"location": "Server Room A, Rack 3",
"ok": true
}Send a command to an outlet.
URL parameters:
| Parameter | Description |
|---|---|
n |
Outlet number (1-based) |
Query parameters: device_id (optional, required for multi-PDU)
Request body:
{
"action": "off"
}Valid actions: on, off, reboot
Response (200):
{
"outlet": 3,
"action": "off",
"device_id": "pdu44001",
"ok": true
}Error responses:
| Status | Condition |
|---|---|
| 400 | Invalid outlet number or action |
| 500 | SNMP SET failed |
| 503 | Command handler not available |
Set a custom name for an outlet. The name persists across restarts.
Request body:
{
"name": "File Server"
}To clear a custom name and revert to the PDU hardware name:
{
"name": ""
}Response (200):
{
"outlet": 1,
"name": "File Server",
"ok": true
}Get all custom outlet name overrides.
Response (200):
{
"1": "File Server",
"3": "Network Switch",
"5": "Lab Equipment"
}List all automation rules with their current state.
Query parameters: device_id (optional)
Response (200):
[
{
"name": "low-voltage-shutoff",
"input": 1,
"condition": "voltage_below",
"threshold": 108.0,
"outlet": 5,
"action": "off",
"restore": true,
"delay": 5,
"state": {
"triggered": false,
"condition_since": null,
"fired_at": null
}
}
]Create a new automation rule.
Query parameters: device_id (optional)
Request body:
{
"name": "brownout-protection",
"input": 1,
"condition": "voltage_below",
"threshold": 108,
"outlet": 5,
"action": "off",
"restore": true,
"delay": 10
}Response (201): The created rule object.
Error responses:
| Status | Condition |
|---|---|
| 400 | Invalid rule data (missing fields, bad condition, etc.) |
| 409 | Rule with that name already exists |
Update an existing rule. The rule name in the URL must match an existing rule.
Query parameters: device_id (optional)
Request body: Same fields as POST (the name field in the body is ignored; the URL name is used).
Response (200): The updated rule object.
Error responses:
| Status | Condition |
|---|---|
| 400 | Invalid rule data |
| 404 | Rule not found |
Delete an automation rule.
Query parameters: device_id (optional)
Response (200):
{
"deleted": "brownout-protection"
}Error: 404 if rule not found.
Get the automation event log (most recent first, up to 100 events).
Query parameters: device_id (optional)
Response (200):
[
{
"rule": "low-voltage-shutoff",
"type": "triggered",
"details": "Input 1 voltage_below 108 -> outlet 5 off",
"ts": 1708531200.0
},
{
"rule": "low-voltage-shutoff",
"type": "restored",
"details": "Input 1 recovered -> outlet 5 on",
"ts": 1708531260.0
}
]Event types: triggered, restored, created, updated, deleted.
Enable or disable an automation rule without deleting it.
Query parameters: device_id (optional)
Request body:
{
"enabled": false
}Response (200): The updated rule object.
Error: 404 if rule not found.
All history endpoints support time range filtering and automatic downsampling.
| Parameter | Description | Example |
|---|---|---|
range |
Preset time range | 1h, 6h, 24h, 7d, 30d |
start |
Unix timestamp (start) | 1708444800 |
end |
Unix timestamp (end) | 1708531200 |
If both start and end are provided, they take precedence over range. The default range is 1h.
The bridge automatically downsamples data based on the time range to keep responses fast:
| Range | Sample Interval | Max Points |
|---|---|---|
| Up to 1 hour | 1 second (raw) | 3,600 |
| Up to 6 hours | 10 seconds | 2,160 |
| Up to 24 hours | 1 minute | 1,440 |
| Up to 7 days | 5 minutes | 2,016 |
| Up to 30 days | 15 minutes | 2,880 |
| 60 days | 30 minutes | 2,880 |
Query bank history data (voltage, current, power, apparent power, power factor).
Query parameters: range or start/end, device_id (optional)
Response (200):
[
{
"bucket": 1708531200,
"bank": 1,
"voltage": 121.3,
"current": 1.2,
"power": 145.0,
"apparent": 150.0,
"pf": 0.97
}
]Query outlet history data (current, power, energy).
Query parameters: range or start/end, device_id (optional)
Response (200):
[
{
"bucket": 1708531200,
"outlet": 1,
"current": 0.8,
"power": 95.0,
"energy": 12.5
}
]Same data as /api/history/banks, returned as a CSV file download.
Headers: Content-Disposition: attachment; filename="bank_history.csv"
Columns: bucket, bank, voltage, current, power, apparent, pf
Same data as /api/history/outlets, returned as a CSV file download.
Headers: Content-Disposition: attachment; filename="outlet_history.csv"
Columns: bucket, outlet, current, power, energy
These endpoints serve daily and monthly energy rollup data for energy tracking and bill splitting.
Query daily energy rollups.
Query parameters: device_id (optional), start (YYYY-MM-DD), end (YYYY-MM-DD)
Response (200):
[
{
"date": "2026-02-16",
"device_id": "pdu44001",
"source": null,
"outlet": null,
"kwh": 2.912,
"peak_power_w": 280,
"avg_power_w": 121,
"samples": 86400
}
]Rows with source=null, outlet=null are daily totals. Rows with source=1 or source=2 break down by power source. Rows with outlet=N break down by outlet.
Query monthly energy rollups.
Query parameters: device_id (optional), start (YYYY-MM), end (YYYY-MM)
Response (200):
[
{
"month": "2026-01",
"device_id": "pdu44001",
"source": null,
"outlet": null,
"kwh": 85.4,
"peak_power_w": 310,
"avg_power_w": 115,
"days": 31
}
]Get an energy usage summary combining live and historical data.
Query parameters: device_id (optional)
Response (200):
{
"today_kwh": 1.23,
"week_kwh": 8.5,
"month_kwh": 45.2,
"source_a_kwh": 28.1,
"source_b_kwh": 17.1
}Same data as /api/energy/daily, returned as a CSV file download.
Same data as /api/energy/monthly, returned as a CSV file download.
The bridge auto-generates weekly and monthly energy reports as PDF files. Reports are stored in the reports directory (default: /data/reports, host-mounted to ./reports/). On-demand generation is also available via the web UI or API.
List all available PDF reports.
Query parameters: device_id (optional) — filter by device
Response (200):
{
"reports": [
{
"filename": "pdu-rack-1_weekly_2026-02-16.pdf",
"device_id": "pdu-rack-1",
"report_type": "weekly",
"period": "2026-02-16",
"size_bytes": 36730,
"created": "2026-02-23 06:00"
},
{
"filename": "pdu-rack-1_monthly_2026-01.pdf",
"device_id": "pdu-rack-1",
"report_type": "monthly",
"period": "2026-01",
"size_bytes": 42150,
"created": "2026-02-01 01:15"
}
],
"count": 2
}Generate a report on demand.
Request body:
{
"device_id": "pdu-rack-1",
"type": "weekly",
"period": "2026-02-16"
}type—"weekly"or"monthly"period— For weekly: Monday date asYYYY-MM-DD. For monthly:YYYY-MM. Omit for the most recent completed period.
Response (200):
{
"ok": true,
"path": "/data/reports/pdu-rack-1_weekly_2026-02-16.pdf",
"filename": "pdu-rack-1_weekly_2026-02-16.pdf"
}Error (400): {"ok": false, "error": "No energy data for requested period"}
Download a PDF report file.
Response (200): PDF file with Content-Disposition: attachment header.
Error (404): {"error": "Report not found"} — returned for missing files or path traversal attempts.
These endpoints read and write PDU configuration via the serial console (or MockPDU in mock mode). They require a serial transport or mock transport to be active; otherwise they return 503.
All management endpoints accept an optional ?device_id= query parameter for multi-PDU setups.
Get device and bank load thresholds.
Response (200):
{
"device": {
"overload_threshold": 80,
"near_overload_threshold": 70,
"low_load_threshold": 10
},
"banks": {
"1": {"overload": 80, "near_overload": 70, "low_load": 10},
"2": {"overload": 80, "near_overload": 70, "low_load": 10}
}
}Set device-level load thresholds.
Request body:
{
"overload": 80,
"near_overload": 70,
"low_load": 10
}Response (200): {"ok": true}
Set thresholds for a specific bank.
Request body: Same as device thresholds.
Get outlet configuration (names, delays, bank assignments).
Response (200):
{
"outlets": {
"1": {"name": "Outlet 1", "on_delay": 0, "off_delay": 0, "bank": 1},
"2": {"name": "Outlet 2", "on_delay": 0, "off_delay": 0, "bank": 1}
}
}Configure a specific outlet (name, on_delay, off_delay).
Request body:
{
"name": "File Server",
"on_delay": 5,
"off_delay": 0
}Get PDU network configuration.
Response (200):
{
"ip": "192.168.1.100",
"subnet": "255.255.255.0",
"gateway": "192.168.1.1",
"dhcp_enabled": false,
"mac_address": "00:11:22:33:44:55"
}Write network configuration to the PDU.
Request body:
{
"ip": "192.168.1.100",
"subnet": "255.255.255.0",
"gateway": "192.168.1.1",
"dhcp": false
}Get ATS (Automatic Transfer Switch) configuration.
Response (200):
{
"preferred_source": "A",
"voltage_sensitivity": "Normal",
"transfer_voltage": 96,
"voltage_upper_limit": 138,
"voltage_lower_limit": 96,
"auto_transfer": true,
"coldstart_delay": 0,
"coldstart_state": "on"
}Request body: {"source": "A"} (or "B")
Request body: {"sensitivity": "normal"} (or "high", "low")
Request body: {"upper": 138, "lower": 96}
Request body: {"enabled": true}
Request body: {"delay": 0, "state": "on"}
Check if the PDU is using factory default credentials (cyber/cyber).
Response (200):
{
"default_credentials": true
}Change a PDU user account password.
Request body:
{
"account": "admin",
"password": "NewSecureP@ss"
}Response (200): {"ok": true}
Get PDU user accounts.
Response (200):
{
"users": {
"admin": {"access": "admin"},
"device": {"access": "viewer"}
}
}Get the PDU event log.
Response (200):
{
"events": [
{"timestamp": "02/23/2026 10:00:00", "event_type": "System", "description": "System Started"}
]
}Get notification configuration (traps, SMTP, email, syslog).
Response (200):
{
"traps": [
{"index": 1, "ip": "0.0.0.0", "community": "public", "type": "v2c", "enabled": true}
],
"smtp": {"server": "", "port": 25, "from_addr": "", "auth_user": ""},
"email": [
{"index": 1, "to": "", "enabled": false}
],
"syslog": [
{"index": 1, "ip": "0.0.0.0", "port": 514, "enabled": false}
]
}Update a trap receiver.
Update SMTP server configuration.
Update an email recipient.
Update a syslog server.
Get EnergyWise configuration.
Response (200):
{
"domain": "",
"port": 43440,
"enabled": false
}Update EnergyWise configuration.
Request body:
{
"domain": "energywise.local",
"port": 43440,
"enabled": true
}Test SNMP connectivity to a PDU without adding it to the configuration.
Request body:
{
"host": "192.168.20.177",
"snmp_port": 161,
"community_read": "public"
}Response (200):
{
"ok": true,
"identity": {
"model": "PDU44001",
"serial": "ABC123",
"name": "CyberPower PDU"
}
}Error: 503 if test callback is not configured.
Test serial connectivity to a PDU.
Request body:
{
"port": "/dev/ttyUSB0",
"baud": 9600,
"username": "cyber",
"password": "cyber"
}Response (200):
{
"ok": true,
"identity": {
"model": "PDU44001",
"name": "CyberPower PDU"
}
}These are only active when BRIDGE_WEB_PASSWORD is set.
Request body: {"password": "your-password"}
Response (200): {"ok": true, "token": "..."} with a session cookie.
Invalidate the current session.
Check if the current session is authenticated.
Server-Sent Events (SSE) stream for real-time dashboard updates. The bridge pushes a JSON event every poll cycle with the full PDU status. Connect from JavaScript with new EventSource('/api/stream').
Response: text/event-stream with data: {...} JSON payloads.
System information including uptime, PDU count, and Python version.
Response (200):
{
"uptime_seconds": 3600.5,
"pdu_count": 1,
"python_version": "3.12.3",
"restart_required": false
}Query bridge log entries from the in-memory ring buffer.
Query parameters:
| Parameter | Description | Default |
|---|---|---|
level |
Minimum log level (DEBUG, INFO, WARNING, ERROR) |
INFO |
search |
Text search filter (case-insensitive) | (none) |
limit |
Max entries to return | 200 |
Response (200):
{
"logs": [
{"ts": "2026-02-23 10:00:01", "level": "INFO", "message": "[pdu44001] Poll #1: voltage=120.4V"}
],
"count": 1
}Restart the bridge process. Returns immediately; the process exits after a short delay.
Response (200): {"ok": true}
Download a JSON backup of all bridge configuration (rules, outlet names, PDU config, settings).
Response (200): JSON file with Content-Disposition: attachment header.
Restore bridge configuration from a backup JSON file. Sets restart_required flag.
Request body: JSON object with filenames as keys and file contents as values.
Response (200): {"ok": true, "restored": ["rules.json", "outlet_names.json"]}
Serves the web dashboard (bridge/static/index.html).