feat: OpenAPI 3.0 spec with Swagger UI

- GET /openapi.yaml: raw OpenAPI spec
- GET /openapi.json: JSON-formatted spec
- GET /docs: Swagger UI for interactive API docs
- 19 endpoints documented with schemas
- Added pyyaml dependency
This commit is contained in:
user
2026-02-05 21:30:09 +01:00
parent b36b1579c7
commit 5672c0c22e
4 changed files with 878 additions and 2 deletions

View File

@@ -16,7 +16,7 @@
### P3 - Low ### P3 - Low
- [ ] Add pagination to all list endpoints - [ ] Add pagination to all list endpoints
- [ ] Add OpenAPI/Swagger spec - [x] Add OpenAPI/Swagger spec
- [ ] Add request logging middleware - [ ] Add request logging middleware
## Completed: v0.1.2 - OSINT Features ## Completed: v0.1.2 - OSINT Features

View File

@@ -10,6 +10,7 @@ dependencies = [
"flask-cors>=4.0", "flask-cors>=4.0",
"gunicorn>=21.0", "gunicorn>=21.0",
"python-dotenv>=1.0", "python-dotenv>=1.0",
"pyyaml>=6.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -1,7 +1,7 @@
"""ESP32-Web Flask Application.""" """ESP32-Web Flask Application."""
import click import click
from datetime import datetime, UTC from datetime import datetime, UTC
from flask import Flask from flask import Flask, Response, send_from_directory
from pathlib import Path from pathlib import Path
from .config import Config from .config import Config
@@ -42,6 +42,45 @@ def create_app(config_class=Config):
uptime_str = f'{minutes}m{seconds}s' uptime_str = f'{minutes}m{seconds}s'
return {'status': 'ok', 'uptime': uptime_str, 'uptime_seconds': uptime_seconds} return {'status': 'ok', 'uptime': uptime_str, 'uptime_seconds': uptime_seconds}
# OpenAPI spec endpoints
@app.route('/openapi.yaml')
def openapi_yaml():
"""Serve OpenAPI spec as YAML."""
spec_path = Path(__file__).parent / 'openapi.yaml'
return Response(spec_path.read_text(), mimetype='text/yaml')
@app.route('/openapi.json')
def openapi_json():
"""Serve OpenAPI spec as JSON."""
import json
import yaml
spec_path = Path(__file__).parent / 'openapi.yaml'
spec = yaml.safe_load(spec_path.read_text())
return spec
@app.route('/docs')
def swagger_ui():
"""Serve Swagger UI."""
return '''<!DOCTYPE html>
<html>
<head>
<title>ESP32-Web API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: "BaseLayout"
});
</script>
</body>
</html>'''
# Start UDP collector in non-testing mode # Start UDP collector in non-testing mode
if not app.config.get('TESTING'): if not app.config.get('TESTING'):
from .collector import collector from .collector import collector

836
src/esp32_web/openapi.yaml Normal file
View File

@@ -0,0 +1,836 @@
openapi: 3.0.3
info:
title: ESP32-Web API
description: REST API for ESP32 sensor fleet management
version: 0.1.3
contact:
name: ESP32-Web
servers:
- url: /api/v1
description: API v1
tags:
- name: sensors
description: Sensor management and fleet operations
- name: devices
description: Discovered BLE/WiFi devices
- name: alerts
description: Security alerts and anomalies
- name: events
description: Sensor events (motion, presence)
- name: probes
description: WiFi probe requests
- name: stats
description: Aggregate statistics
- name: export
description: Data export endpoints
paths:
/sensors:
get:
tags: [sensors]
summary: List all sensors
operationId: listSensors
responses:
'200':
description: List of sensors
content:
application/json:
schema:
type: object
properties:
sensors:
type: array
items:
$ref: '#/components/schemas/Sensor'
/sensors/heartbeat:
get:
tags: [sensors]
summary: Get heartbeat status for all sensors
operationId: getHeartbeatStatus
responses:
'200':
description: Heartbeat summary
content:
application/json:
schema:
$ref: '#/components/schemas/HeartbeatSummary'
post:
tags: [sensors]
summary: Refresh heartbeat status for all sensors
operationId: refreshHeartbeats
responses:
'200':
description: Update result
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: updated
online:
type: integer
stale:
type: integer
offline:
type: integer
/sensors/{hostname}:
get:
tags: [sensors]
summary: Get sensor by hostname
operationId: getSensor
parameters:
- $ref: '#/components/parameters/hostname'
responses:
'200':
description: Sensor details
content:
application/json:
schema:
$ref: '#/components/schemas/Sensor'
'404':
$ref: '#/components/responses/NotFound'
/sensors/{hostname}/command:
post:
tags: [sensors]
summary: Send UDP command to sensor
operationId: sendCommand
parameters:
- $ref: '#/components/parameters/hostname'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [command]
properties:
command:
type: string
example: STATUS
responses:
'200':
description: Command sent
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: sent
command:
type: string
'403':
description: Command not allowed
'404':
$ref: '#/components/responses/NotFound'
/sensors/{hostname}/config:
get:
tags: [sensors]
summary: Get sensor configuration
operationId: getSensorConfig
parameters:
- $ref: '#/components/parameters/hostname'
responses:
'200':
description: Sensor configuration
content:
application/json:
schema:
type: object
properties:
config:
$ref: '#/components/schemas/SensorConfig'
'404':
$ref: '#/components/responses/NotFound'
'504':
description: Sensor not responding
put:
tags: [sensors]
summary: Update sensor configuration
operationId: updateSensorConfig
parameters:
- $ref: '#/components/parameters/hostname'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
rate:
type: integer
description: CSI send rate (Hz)
power:
type: integer
description: TX power (dBm)
adaptive:
type: boolean
threshold:
type: number
ble:
type: boolean
csi_mode:
type: string
enum: [raw, amplitude, hybrid]
presence:
type: boolean
powersave:
type: boolean
chanscan:
type: boolean
responses:
'200':
description: Update results
content:
application/json:
schema:
type: object
properties:
results:
type: object
errors:
type: array
items:
type: string
'404':
$ref: '#/components/responses/NotFound'
/sensors/{hostname}/ota:
post:
tags: [sensors]
summary: Trigger OTA update
operationId: triggerOta
parameters:
- $ref: '#/components/parameters/hostname'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [url]
properties:
url:
type: string
format: uri
example: https://example.com/firmware.bin
responses:
'200':
description: OTA triggered
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ota_triggered
url:
type: string
'400':
description: Invalid URL
'404':
$ref: '#/components/responses/NotFound'
/sensors/{hostname}/calibrate:
post:
tags: [sensors]
summary: Trigger baseline calibration
operationId: triggerCalibrate
parameters:
- $ref: '#/components/parameters/hostname'
requestBody:
content:
application/json:
schema:
type: object
properties:
seconds:
type: integer
minimum: 3
maximum: 60
default: 10
responses:
'200':
description: Calibration started
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: calibration_started
seconds:
type: integer
'400':
description: Invalid seconds value
'404':
$ref: '#/components/responses/NotFound'
/sensors/{hostname}/metrics:
get:
tags: [sensors]
summary: Get sensor activity metrics
operationId: getSensorMetrics
parameters:
- $ref: '#/components/parameters/hostname'
- name: hours
in: query
schema:
type: integer
default: 24
minimum: 1
maximum: 168
description: Time range in hours (max 168)
responses:
'200':
description: Sensor metrics
content:
application/json:
schema:
$ref: '#/components/schemas/SensorMetrics'
'404':
$ref: '#/components/responses/NotFound'
/devices:
get:
tags: [devices]
summary: List discovered devices
operationId: listDevices
parameters:
- name: type
in: query
schema:
type: string
enum: [ble, wifi]
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: List of devices
content:
application/json:
schema:
type: object
properties:
devices:
type: array
items:
$ref: '#/components/schemas/Device'
limit:
type: integer
offset:
type: integer
/devices/{mac}:
get:
tags: [devices]
summary: Get device by MAC address
operationId: getDevice
parameters:
- name: mac
in: path
required: true
schema:
type: string
example: aa:bb:cc:dd:ee:ff
responses:
'200':
description: Device details with sightings
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Device'
- type: object
properties:
sightings:
type: array
items:
$ref: '#/components/schemas/Sighting'
'404':
$ref: '#/components/responses/NotFound'
/alerts:
get:
tags: [alerts]
summary: List security alerts
operationId: listAlerts
parameters:
- name: type
in: query
schema:
type: string
- name: sensor_id
in: query
schema:
type: integer
- $ref: '#/components/parameters/hours'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: List of alerts
content:
application/json:
schema:
type: object
properties:
alerts:
type: array
items:
$ref: '#/components/schemas/Alert'
limit:
type: integer
offset:
type: integer
/events:
get:
tags: [events]
summary: List sensor events
operationId: listEvents
parameters:
- name: type
in: query
schema:
type: string
- name: sensor_id
in: query
schema:
type: integer
- $ref: '#/components/parameters/hours'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: List of events
content:
application/json:
schema:
type: object
properties:
events:
type: array
items:
$ref: '#/components/schemas/Event'
limit:
type: integer
offset:
type: integer
/probes:
get:
tags: [probes]
summary: List probe requests
operationId: listProbes
parameters:
- name: ssid
in: query
schema:
type: string
- $ref: '#/components/parameters/hours'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: List of probe requests
content:
application/json:
schema:
type: object
properties:
probes:
type: array
items:
$ref: '#/components/schemas/Probe'
limit:
type: integer
offset:
type: integer
/probes/ssids:
get:
tags: [probes]
summary: List SSIDs with counts
operationId: listSsids
parameters:
- $ref: '#/components/parameters/hours'
responses:
'200':
description: SSID list with counts
content:
application/json:
schema:
type: object
properties:
ssids:
type: array
items:
type: object
properties:
ssid:
type: string
count:
type: integer
/stats:
get:
tags: [stats]
summary: Get aggregate statistics
operationId: getStats
parameters:
- $ref: '#/components/parameters/hours'
responses:
'200':
description: Statistics
content:
application/json:
schema:
$ref: '#/components/schemas/Stats'
/export/devices.csv:
get:
tags: [export]
summary: Export devices as CSV
operationId: exportDevicesCsv
responses:
'200':
description: CSV file
content:
text/csv:
schema:
type: string
/export/devices.json:
get:
tags: [export]
summary: Export devices as JSON
operationId: exportDevicesJson
responses:
'200':
description: JSON file
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Device'
/export/alerts.csv:
get:
tags: [export]
summary: Export alerts as CSV
operationId: exportAlertsCsv
parameters:
- $ref: '#/components/parameters/hours'
responses:
'200':
description: CSV file
content:
text/csv:
schema:
type: string
/export/probes.csv:
get:
tags: [export]
summary: Export probes as CSV
operationId: exportProbesCsv
parameters:
- $ref: '#/components/parameters/hours'
responses:
'200':
description: CSV file
content:
text/csv:
schema:
type: string
components:
parameters:
hostname:
name: hostname
in: path
required: true
schema:
type: string
example: muddy-storm
limit:
name: limit
in: query
schema:
type: integer
default: 100
maximum: 1000
offset:
name: offset
in: query
schema:
type: integer
default: 0
hours:
name: hours
in: query
schema:
type: integer
default: 24
responses:
NotFound:
description: Resource not found
content:
application/json:
schema:
type: object
properties:
error:
type: string
schemas:
Sensor:
type: object
properties:
id:
type: integer
hostname:
type: string
ip:
type: string
last_seen:
type: string
format: date-time
status:
type: string
enum: [online, stale, offline, unknown]
SensorConfig:
type: object
properties:
hostname:
type: string
ip:
type: string
version:
type: string
uptime:
type: string
rate:
type: integer
tx_power:
type: integer
adaptive:
type: boolean
ble:
type: boolean
presence:
type: boolean
powersave:
type: boolean
csi_mode:
type: string
channel:
type: integer
rssi:
type: number
heap:
type: integer
SensorMetrics:
type: object
properties:
hostname:
type: string
hours:
type: integer
activity:
type: object
properties:
sightings:
type: integer
alerts:
type: integer
events:
type: integer
recent_events:
type: array
items:
$ref: '#/components/schemas/Event'
HeartbeatSummary:
type: object
properties:
total:
type: integer
online:
type: integer
stale:
type: integer
offline:
type: integer
sensors:
type: array
items:
type: object
properties:
hostname:
type: string
ip:
type: string
status:
type: string
last_seen:
type: string
format: date-time
seconds_ago:
type: integer
Device:
type: object
properties:
id:
type: integer
mac:
type: string
device_type:
type: string
enum: [ble, wifi]
vendor:
type: string
nullable: true
name:
type: string
nullable: true
company_id:
type: integer
nullable: true
manufacturer:
type: string
nullable: true
first_seen:
type: string
format: date-time
last_seen:
type: string
format: date-time
Sighting:
type: object
properties:
id:
type: integer
device_id:
type: integer
sensor_id:
type: integer
rssi:
type: integer
timestamp:
type: string
format: date-time
Alert:
type: object
properties:
id:
type: integer
sensor_id:
type: integer
alert_type:
type: string
source_mac:
type: string
nullable: true
target_mac:
type: string
nullable: true
rssi:
type: integer
nullable: true
timestamp:
type: string
format: date-time
Event:
type: object
properties:
id:
type: integer
sensor_id:
type: integer
type:
type: string
payload:
type: object
timestamp:
type: string
format: date-time
Probe:
type: object
properties:
id:
type: integer
sensor_id:
type: integer
device_id:
type: integer
ssid:
type: string
rssi:
type: integer
channel:
type: integer
timestamp:
type: string
format: date-time
Stats:
type: object
properties:
sensors:
type: object
properties:
total:
type: integer
online:
type: integer
devices:
type: object
properties:
total:
type: integer
ble:
type: integer
wifi:
type: integer
alerts:
type: object
properties:
count:
type: integer
hours:
type: integer
events:
type: object
properties:
count:
type: integer
hours:
type: integer
probes:
type: object
properties:
count:
type: integer
hours:
type: integer