REST API¶
All endpoints are on port 80. Base URL: http://<device-ip>/api
Endpoints¶
GET /api/status¶
System status, firmware metadata, memory, flash, filesystem, partition, time, WiFi, sensor, GPS, and MQTT diagnostics.
{
"uptime": 3600,
"freeHeap": 210432,
"heapSize": 327680,
"cpuFreqMHz": 240,
"flashSize": 4194304,
"sketchSize": 1048576,
"freeSketchSpace": 786432,
"fsTotal": 196608,
"fsUsed": 40960,
"firmware": {
"name": "SQMeter",
"version": "0.0.1",
"buildDate": "Apr 25 2026",
"buildTime": "12:00:00"
},
"partitions": {
"runningSlot": "app0",
"runningAddress": 65536,
"runningSize": 1966080,
"bootSlot": "app0",
"nextSlot": "app1",
"nextSize": 1966080,
"fsAddress": 3997696,
"fsSize": 196608,
"nvs": {
"usedEntries": 24,
"freeEntries": 96,
"totalEntries": 120,
"namespaceCount": 2
}
},
"time": {
"iso": "2026-04-25T22:14:00+0000",
"timezone": "UTC0"
},
"ntp": {
"enabled": true,
"synced": true,
"status": 2,
"lastSync": 120000,
"nextSync": 720000,
"drift": 0,
"server": "pool.ntp.org",
"activeSource": 1,
"gpsEnabled": false,
"gpsHasFix": false,
"gpsTimeUTC": "",
"gpsSatellites": 0
},
"wifi": {
"connected": true,
"ssid": "MyNetwork",
"ip": "192.168.1.42",
"rssi": -62,
"mac": "AA:BB:CC:DD:EE:FF"
},
"sensors": {
"tsl2591": { "initialized": true, "status": 0, "lastUpdate": 3595000 },
"bme280": { "initialized": true, "status": 0, "lastUpdate": 3595000 },
"mlx90614": { "initialized": true, "status": 0, "lastUpdate": 3595000 },
"gps": { "initialized": false, "status": 1, "lastUpdate": 0 },
"rg15": {
"enabled": true,
"initialized": true,
"online": true,
"stale": false,
"state": "online",
"status": 0,
"lastUpdate": 3595000,
"uart": {
"last_command": "R",
"last_raw_response": "Acc 0.00 mm, EventAcc 0.00 mm, TotalAcc 1.24 mm, RInt 0.00 mm/h",
"timeouts": 0,
"parse_errors": 0,
"successful_reads": 42
}
}
},
"mqtt": {
"enabled": false,
"connected": false,
"state": -1,
"lastPublish": 0,
"lastReconnectAttempt": 0,
"broker": "",
"port": 1883,
"topic": "sqm/data"
}
}
Known diagnostic gaps
The firmware now exposes RG-15 UART diagnostics and sensor freshness information, but it still does not expose every possible system metric such as reset reason, boot count, or minFreeHeap. Use serial logs for deeper system bring-up diagnostics.
GET /api/sensors¶
Current sensor readings (point-in-time snapshot).
{
"lightSensor": {
"lux": 0.0234,
"rawLux": 0.0231,
"visible": 123,
"infrared": 45,
"full": 168,
"gainName": "MAX",
"gainFactor": 9876,
"integrationMs": 600,
"averagingWindowSeconds": 90,
"calibrated": true,
"saturated": false,
"status": 0
},
"skyQuality": {
"sqm": 21.5,
"rawSqm": 21.42,
"calibratedSqm": 21.5,
"nelm": 6.2,
"bortle": 2.0,
"description": "Typical truly dark site",
"nightMode": true
},
"lightDiagnostics": {
"rollingVisible": 123.4,
"correctedVisible": 121.9,
"darkVisibleOffset": 1.5,
"sampleCount": 138,
"rejectedSamples": 0
},
"environment": {
"temperature": 12.4,
"humidity": 72.1,
"pressure": 1013.25,
"dewpoint": 7.8,
"status": 0
},
"irTemperature": {
"objectTemp": -15.2,
"ambientTemp": 12.4,
"status": 0
},
"cloudConditions": {
"temperatureDelta": -27.6,
"correctedDelta": -24.1,
"cloudCoverPercent": 5.0,
"condition": 0,
"description": "Clear",
"humidityUsed": 72.1
},
"gps": {
"hasFix": true,
"satellites": 8,
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 42.0,
"hdop": 1.2,
"age": 800
},
"rainSensor": {
"enabled": true,
"sensor": "hydreon_rg15",
"initialized": true,
"online": true,
"stale": false,
"state": "online",
"timestamp": 1234567890,
"ageMs": 40,
"status": 0,
"isRaining": false,
"raining": false,
"acc": 0.000,
"eventAcc": 2.400,
"event_accumulation": 0.000,
"hydreon_event_accumulation": 2.400,
"totalAcc": 12.340,
"rInt": 0.000,
"lensBad": false,
"emSat": false,
"uart": {
"configured": true,
"opened": true,
"rx_pin": 18,
"tx_pin": 19,
"baud_rate": 9600,
"uart_port": 1,
"mode": "polling",
"resolution": "high",
"units": "metric",
"debug_uart": false,
"last_command": "R",
"last_raw_response": "Acc 0.00 mm, EventAcc 0.00 mm, TotalAcc 1.24 mm, RInt 0.00 mm/h",
"last_error": null,
"timeouts": 0,
"parse_errors": 0,
"successful_reads": 42
}
}
}
POST /api/sensors/tsl2591/calibrate-dark¶
Stores the current rolling TSL2591 visible count as the dark visible offset. Cover the aperture with an opaque cap, wait for the rolling window to fill, then call this endpoint.
Optional fields
The gps object is only present if a GPS module is connected and initialised. The rainSensor object is present whenever the RG-15 path is compiled into the firmware; check enabled, initialized, and online to distinguish disabled, opened, and live sensor states. All other objects are always present.
status values¶
| Value | Meaning |
|---|---|
0 |
OK |
1 |
Sensor not found |
2 |
Read error |
3 |
Stale data |
POST /api/sensors/rg15/test¶
Trigger a manual RG-15 read and return communication diagnostics.
When HTTP auth is enabled, this endpoint requires credentials.
The response includes:
commandbytes_writtenraw_responseackacknowledgedparsedelapsed_mserrorhint
GET /api/config¶
Read the full device configuration. Password fields are returned as ******** when a stored password exists.
LAN-sensitive endpoint
Password fields are redacted in this response, but the endpoint is still unauthenticated and returns operational configuration. Treat /api/config as LAN-sensitive and do not expose the device to guest networks or the public internet.
POST /api/config¶
Update configuration. Partial updates are supported — only the fields you send are changed.
curl -X POST http://sqm-esp32.local/api/config \
-H "Content-Type: application/json" \
-d '{"deviceName": "backyard-sqm", "sensor": {"readIntervalMs": 10000}}'
Masked or empty password fields preserve the existing stored password. Send null for wifi.password, mqtt.password, or ota.password only when you intentionally want to clear that password.
Successful saves return:
Note
POST is the primary method. The firmware also accepts PUT for compatibility with full-resource settings clients.
GET /api/wifi/scan¶
Scan for nearby WiFi networks.
{
"networks": [
{ "ssid": "MyNetwork", "rssi": -55, "encryption": "secured" },
{ "ssid": "Neighbour", "rssi": -80, "encryption": "secured" }
]
}
Blocking scan
This endpoint currently calls WiFi.scanNetworks() synchronously inside the async web-server handler. Avoid repeated scans while streaming WebSocket data or performing OTA updates.
POST /api/wifi/connect¶
Connect to a WiFi network and save credentials to NVS.
curl -X POST http://sqm-esp32.local/api/wifi/connect \
-H "Content-Type: application/json" \
-d '{"ssid": "MyNetwork", "password": "hunter2"}'
POST /api/restart¶
Restart the device.
POST /api/update¶
OTA firmware update. Send a raw .bin file as multipart/form-data.
The device reboots automatically on success.
POST /api/update/fs¶
OTA filesystem update. Send a LittleFS image as multipart/form-data.
Use this endpoint for web UI assets only. It does not update firmware and does not erase NVS configuration.
Unauthenticated update endpoints
/api/update and /api/update/fs are LAN-only convenience endpoints and currently have no HTTP authentication. Keep SQMeter on a trusted network and do not port-forward it.