From 5672c0c22e9b323263d707cf4e8782bbd8cec2f1 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 5 Feb 2026 21:30:09 +0100 Subject: [PATCH] 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 --- TASKS.md | 2 +- pyproject.toml | 1 + src/esp32_web/__init__.py | 41 +- src/esp32_web/openapi.yaml | 836 +++++++++++++++++++++++++++++++++++++ 4 files changed, 878 insertions(+), 2 deletions(-) create mode 100644 src/esp32_web/openapi.yaml diff --git a/TASKS.md b/TASKS.md index af63b23..340c470 100644 --- a/TASKS.md +++ b/TASKS.md @@ -16,7 +16,7 @@ ### P3 - Low - [ ] Add pagination to all list endpoints -- [ ] Add OpenAPI/Swagger spec +- [x] Add OpenAPI/Swagger spec - [ ] Add request logging middleware ## Completed: v0.1.2 - OSINT Features diff --git a/pyproject.toml b/pyproject.toml index e89444f..d696469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "flask-cors>=4.0", "gunicorn>=21.0", "python-dotenv>=1.0", + "pyyaml>=6.0", ] [project.optional-dependencies] diff --git a/src/esp32_web/__init__.py b/src/esp32_web/__init__.py index dfe1387..f8165ca 100644 --- a/src/esp32_web/__init__.py +++ b/src/esp32_web/__init__.py @@ -1,7 +1,7 @@ """ESP32-Web Flask Application.""" import click from datetime import datetime, UTC -from flask import Flask +from flask import Flask, Response, send_from_directory from pathlib import Path from .config import Config @@ -42,6 +42,45 @@ def create_app(config_class=Config): uptime_str = f'{minutes}m{seconds}s' 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 ''' + + + ESP32-Web API + + + +
+ + + +''' + # Start UDP collector in non-testing mode if not app.config.get('TESTING'): from .collector import collector diff --git a/src/esp32_web/openapi.yaml b/src/esp32_web/openapi.yaml new file mode 100644 index 0000000..4721efc --- /dev/null +++ b/src/esp32_web/openapi.yaml @@ -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