Add versioned HTTP API routes
This commit is contained in:
+58
-50
@@ -8,6 +8,12 @@ Default AP address:
|
||||
http://192.168.4.1
|
||||
```
|
||||
|
||||
Current API base path:
|
||||
|
||||
```text
|
||||
/api/v1
|
||||
```
|
||||
|
||||
All documented endpoints are local-first and must continue to work without internet access. Consumers should ignore unknown response fields for forward compatibility.
|
||||
|
||||
Current firmware version:
|
||||
@@ -20,34 +26,36 @@ Current firmware version:
|
||||
|
||||
```text
|
||||
GET /
|
||||
GET /status
|
||||
GET /config
|
||||
GET /api/v1/status
|
||||
GET /api/v1/config
|
||||
|
||||
POST /relay/set
|
||||
POST /api/v1/relay/set
|
||||
|
||||
POST /config/device
|
||||
POST /config/relay
|
||||
POST /config/temp
|
||||
POST /config/bms
|
||||
POST /config/save
|
||||
POST /config/factory-reset
|
||||
POST /api/v1/config/device
|
||||
POST /api/v1/config/relay
|
||||
POST /api/v1/config/temp
|
||||
POST /api/v1/config/bms
|
||||
POST /api/v1/config/save
|
||||
POST /api/v1/config/factory-reset
|
||||
|
||||
GET /config/wifi
|
||||
POST /config/wifi
|
||||
POST /wifi/connect
|
||||
POST /wifi/clear
|
||||
GET /api/v1/config/wifi
|
||||
POST /api/v1/config/wifi
|
||||
POST /api/v1/wifi/connect
|
||||
POST /api/v1/wifi/clear
|
||||
|
||||
POST /temps/scan
|
||||
POST /temps/assign
|
||||
POST /temps/clear
|
||||
POST /api/v1/temps/scan
|
||||
POST /api/v1/temps/assign
|
||||
POST /api/v1/temps/clear
|
||||
|
||||
POST /bms/setup/enter
|
||||
POST /bms/setup/exit
|
||||
POST /bms/scan
|
||||
POST /bms/select
|
||||
POST /api/v1/bms/setup/enter
|
||||
POST /api/v1/bms/setup/exit
|
||||
POST /api/v1/bms/scan
|
||||
POST /api/v1/bms/select
|
||||
```
|
||||
|
||||
Legacy relay routes remain available:
|
||||
Pre-versioned root routes remain registered as compatibility aliases for existing local clients. New clients should use `/api/v1`.
|
||||
|
||||
Legacy GET relay routes remain available only as root compatibility aliases:
|
||||
|
||||
```text
|
||||
GET /relay/relay_1/on
|
||||
@@ -61,15 +69,15 @@ GET /relay/relay_2/off
|
||||
The Waveshare ESP32-S3 dashboard MVP should use only:
|
||||
|
||||
```text
|
||||
GET /status
|
||||
POST /relay/set
|
||||
GET /api/v1/status
|
||||
POST /api/v1/relay/set
|
||||
```
|
||||
|
||||
The dashboard is a client only. It may cache last-known values for display, but the Cargo ESP32 remains the source of truth for relay state, BMS state, alarms, and configuration.
|
||||
|
||||
## Status
|
||||
|
||||
### GET /status
|
||||
### GET /api/v1/status
|
||||
|
||||
Returns complete controller status.
|
||||
|
||||
@@ -144,7 +152,7 @@ Fields:
|
||||
| `online` | Current sensor detection state |
|
||||
| `temperature_f` | Current Fahrenheit value, or `null` when offline |
|
||||
|
||||
Config supports `temp_1` through `temp_8`. Current `/status` runtime output is limited to the configured count capped at four sensors.
|
||||
Config supports `temp_1` through `temp_8`. Current `/api/v1/status` runtime output is limited to the configured count capped at four sensors.
|
||||
|
||||
### relays
|
||||
|
||||
@@ -228,11 +236,11 @@ The AP remains enabled even when STA WiFi is configured or connected.
|
||||
|
||||
### config
|
||||
|
||||
`/status.config` embeds the same core configuration model used by `GET /config`.
|
||||
`/api/v1/status.config` embeds the same core configuration model used by `GET /api/v1/config`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### GET /config
|
||||
### GET /api/v1/config
|
||||
|
||||
Returns saved controller configuration.
|
||||
|
||||
@@ -264,9 +272,9 @@ Returns saved controller configuration.
|
||||
}
|
||||
```
|
||||
|
||||
Note: current `GET /config` omits the `weather` field, while `/status.config.temperature_sensors[]` includes it.
|
||||
Note: current `GET /api/v1/config` omits the `weather` field, while `/api/v1/status.config.temperature_sensors[]` includes it.
|
||||
|
||||
### POST /config/device
|
||||
### POST /api/v1/config/device
|
||||
|
||||
Updates the controller display name.
|
||||
|
||||
@@ -278,7 +286,7 @@ Updates the controller display name.
|
||||
|
||||
Returns the updated config.
|
||||
|
||||
### POST /config/relay
|
||||
### POST /api/v1/config/relay
|
||||
|
||||
Updates relay configuration.
|
||||
|
||||
@@ -300,7 +308,7 @@ Fields:
|
||||
|
||||
Returns the updated config.
|
||||
|
||||
### POST /config/temp
|
||||
### POST /api/v1/config/temp
|
||||
|
||||
Updates temperature sensor configuration.
|
||||
|
||||
@@ -326,7 +334,7 @@ Fields:
|
||||
|
||||
Returns the updated config.
|
||||
|
||||
### POST /config/bms
|
||||
### POST /api/v1/config/bms
|
||||
|
||||
Updates BMS configuration.
|
||||
|
||||
@@ -348,13 +356,13 @@ random
|
||||
|
||||
Returns the updated config.
|
||||
|
||||
### POST /config/save
|
||||
### POST /api/v1/config/save
|
||||
|
||||
Persists the current active configuration.
|
||||
|
||||
Returns the current config.
|
||||
|
||||
### POST /config/factory-reset
|
||||
### POST /api/v1/config/factory-reset
|
||||
|
||||
Clears saved configuration and restores firmware defaults.
|
||||
|
||||
@@ -362,7 +370,7 @@ Returns the reset config.
|
||||
|
||||
## Relay Control
|
||||
|
||||
### POST /relay/set
|
||||
### POST /api/v1/relay/set
|
||||
|
||||
Preferred relay command endpoint.
|
||||
|
||||
@@ -407,7 +415,7 @@ Response:
|
||||
|
||||
## WiFi
|
||||
|
||||
### GET /config/wifi
|
||||
### GET /api/v1/config/wifi
|
||||
|
||||
Returns AP/STA WiFi configuration status. Passwords are not returned.
|
||||
|
||||
@@ -435,7 +443,7 @@ Returns AP/STA WiFi configuration status. Passwords are not returned.
|
||||
}
|
||||
```
|
||||
|
||||
### POST /config/wifi
|
||||
### POST /api/v1/config/wifi
|
||||
|
||||
Preferred multi-network request:
|
||||
|
||||
@@ -473,13 +481,13 @@ Runtime behavior:
|
||||
- Lower priority numbers are tried first.
|
||||
- If STA disconnects, saved networks are retried by priority.
|
||||
|
||||
### POST /wifi/connect
|
||||
### POST /api/v1/wifi/connect
|
||||
|
||||
Attempts STA connection using saved networks by priority.
|
||||
|
||||
Returns WiFi config status.
|
||||
|
||||
### POST /wifi/clear
|
||||
### POST /api/v1/wifi/clear
|
||||
|
||||
Clears saved STA WiFi networks.
|
||||
|
||||
@@ -487,7 +495,7 @@ Returns WiFi config status.
|
||||
|
||||
## Temperature Probe Setup
|
||||
|
||||
### POST /temps/scan
|
||||
### POST /api/v1/temps/scan
|
||||
|
||||
Scans the DS18B20 bus and returns unassigned probe addresses.
|
||||
|
||||
@@ -506,7 +514,7 @@ Scans the DS18B20 bus and returns unassigned probe addresses.
|
||||
|
||||
Already-assigned probe addresses are hidden from scan results until cleared.
|
||||
|
||||
### POST /temps/assign
|
||||
### POST /api/v1/temps/assign
|
||||
|
||||
Assigns a scanned probe to a logical temperature slot.
|
||||
|
||||
@@ -530,7 +538,7 @@ By slot number:
|
||||
|
||||
Returns the updated config.
|
||||
|
||||
### POST /temps/clear
|
||||
### POST /api/v1/temps/clear
|
||||
|
||||
Clears one or all temperature assignments.
|
||||
|
||||
@@ -560,7 +568,7 @@ Returns the updated config.
|
||||
|
||||
These endpoints are for BMS discovery and selection. Do not change JBD/Xiaoxiang BLE behavior without explicit approval.
|
||||
|
||||
### POST /bms/setup/enter
|
||||
### POST /api/v1/bms/setup/enter
|
||||
|
||||
Enters BMS setup mode.
|
||||
|
||||
@@ -574,7 +582,7 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
### POST /bms/setup/exit
|
||||
### POST /api/v1/bms/setup/exit
|
||||
|
||||
Exits BMS setup mode.
|
||||
|
||||
@@ -588,7 +596,7 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
### POST /bms/scan
|
||||
### POST /api/v1/bms/scan
|
||||
|
||||
Scans for BLE devices.
|
||||
|
||||
@@ -610,7 +618,7 @@ Response:
|
||||
|
||||
Some BMS devices advertise intermittently. Repeated scans may be needed.
|
||||
|
||||
### POST /bms/select
|
||||
### POST /api/v1/bms/select
|
||||
|
||||
Selects a BMS from the most recent scan result.
|
||||
|
||||
@@ -676,9 +684,9 @@ Some serial/UART errors use `message` instead of `error`; HTTP clients should re
|
||||
|
||||
## Compatibility
|
||||
|
||||
Current stable route prefix is the root API path, for example `GET /status`.
|
||||
Current stable API prefix is `/api/v1`, for example `GET /api/v1/status`.
|
||||
|
||||
Future versioning may move to `/api/v1`. Do not introduce versioned routes until consumers and docs are updated together.
|
||||
Pre-versioned root routes remain registered as local compatibility aliases. Do not add new root-only HTTP API routes.
|
||||
|
||||
New fields may be added to existing JSON objects. Consumers should ignore unknown fields.
|
||||
|
||||
@@ -687,8 +695,8 @@ New fields may be added to existing JSON objects. Consumers should ignore unknow
|
||||
These routes are not part of the current registered HTTP API:
|
||||
|
||||
```text
|
||||
POST /bms/reconnect
|
||||
POST /system/restart
|
||||
POST /api/v1/bms/reconnect
|
||||
POST /api/v1/system/restart
|
||||
```
|
||||
|
||||
If added later, update this document and add contract tests.
|
||||
|
||||
@@ -93,11 +93,11 @@ HTTP is the primary integration contract for:
|
||||
|
||||
Preferred direction:
|
||||
|
||||
GET /status
|
||||
GET /config
|
||||
POST /relay/set
|
||||
GET /api/v1/status
|
||||
GET /api/v1/config
|
||||
POST /api/v1/relay/set
|
||||
|
||||
Future versioning may move to /api/v1.
|
||||
Pre-versioned root routes remain registered as compatibility aliases for existing local clients.
|
||||
|
||||
## Generic IDs
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
- WiFi priority and runtime reconnect behavior.
|
||||
- HTTP/UART/USB serial command parity documentation.
|
||||
- DS18B20 configurable temperature probe support.
|
||||
- Preferred relay command endpoint: `POST /relay/set`.
|
||||
- Preferred relay command endpoint: `POST /api/v1/relay/set`.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Serial command:
|
||||
|
||||
HTTP endpoint:
|
||||
|
||||
POST /config/device
|
||||
POST /api/v1/config/device
|
||||
|
||||
## Relay Configuration
|
||||
|
||||
@@ -67,7 +67,7 @@ Serial commands:
|
||||
|
||||
HTTP endpoint:
|
||||
|
||||
POST /config/relay
|
||||
POST /api/v1/config/relay
|
||||
|
||||
## Temperature Sensor Configuration
|
||||
|
||||
@@ -102,7 +102,7 @@ Serial commands:
|
||||
|
||||
HTTP endpoint:
|
||||
|
||||
POST /config/temp
|
||||
POST /api/v1/config/temp
|
||||
|
||||
## BMS Configuration
|
||||
|
||||
@@ -127,7 +127,7 @@ Serial commands:
|
||||
|
||||
HTTP endpoint:
|
||||
|
||||
POST /config/bms
|
||||
POST /api/v1/config/bms
|
||||
|
||||
## BMS Setup Mode
|
||||
|
||||
@@ -157,7 +157,7 @@ Serial command:
|
||||
|
||||
HTTP endpoint:
|
||||
|
||||
POST /config/factory-reset
|
||||
POST /api/v1/config/factory-reset
|
||||
|
||||
After factory reset:
|
||||
|
||||
|
||||
+10
-10
@@ -12,14 +12,14 @@ Control surfaces:
|
||||
|
||||
| Feature | USB Serial | UART JSON | HTTP API | Notes |
|
||||
|---|---:|---:|---:|---|
|
||||
| Status | Yes | Yes | Yes | `/status` equals `status_request` |
|
||||
| Config view | Yes | Yes | Yes | `/config` equals `config_request` |
|
||||
| Relay control | Yes | Yes | Yes | Preferred HTTP endpoint is `POST /relay/set` |
|
||||
| Device name config | Yes | Yes | Yes | `devicename`, `config_device`, `/config/device` |
|
||||
| Relay config | Yes | Yes | Yes | `relayname`, `config_relay`, `/config/relay` |
|
||||
| Temperature config | Yes | Yes | Yes | `tempname`, `config_temp`, `/config/temp` |
|
||||
| BMS config | Yes | Yes | Yes | `bmsname`/`bmsaddr`, `config_bms`, `/config/bms` |
|
||||
| Save config | Yes | Yes | Yes | `save`, `save_config`, `/config/save` |
|
||||
| Status | Yes | Yes | Yes | `/api/v1/status` equals `status_request` |
|
||||
| Config view | Yes | Yes | Yes | `/api/v1/config` equals `config_request` |
|
||||
| Relay control | Yes | Yes | Yes | Preferred HTTP endpoint is `POST /api/v1/relay/set` |
|
||||
| Device name config | Yes | Yes | Yes | `devicename`, `config_device`, `/api/v1/config/device` |
|
||||
| Relay config | Yes | Yes | Yes | `relayname`, `config_relay`, `/api/v1/config/relay` |
|
||||
| Temperature config | Yes | Yes | Yes | `tempname`, `config_temp`, `/api/v1/config/temp` |
|
||||
| BMS config | Yes | Yes | Yes | `bmsname`/`bmsaddr`, `config_bms`, `/api/v1/config/bms` |
|
||||
| Save config | Yes | Yes | Yes | `save`, `save_config`, `/api/v1/config/save` |
|
||||
| Factory reset | Yes | Yes | Yes | AP remains recovery path |
|
||||
| BMS setup enter | Yes | Yes | Yes | Setup pauses normal BMS reconnect behavior |
|
||||
| BMS setup exit | Yes | Yes | Yes | Returns to normal mode |
|
||||
@@ -58,9 +58,9 @@ Legacy UART relay fields may still be accepted:
|
||||
|
||||
Preferred HTTP relay endpoint:
|
||||
|
||||
POST /relay/set
|
||||
POST /api/v1/relay/set
|
||||
|
||||
Legacy HTTP relay routes may remain for compatibility:
|
||||
Legacy root HTTP relay routes remain for compatibility:
|
||||
|
||||
GET /relay/relay_1/on
|
||||
GET /relay/relay_1/off
|
||||
|
||||
@@ -47,15 +47,15 @@ All grounds remain common.
|
||||
## API endpoints consumed
|
||||
|
||||
Minimum dashboard MVP:
|
||||
- GET /status
|
||||
- POST /relay/set
|
||||
- GET /api/v1/status
|
||||
- POST /api/v1/relay/set
|
||||
|
||||
Potential later use:
|
||||
- GET /config
|
||||
- POST /config/save
|
||||
- POST /temps/scan
|
||||
- POST /temps/assign
|
||||
- POST /temps/clear
|
||||
- GET /api/v1/config
|
||||
- POST /api/v1/config/save
|
||||
- POST /api/v1/temps/scan
|
||||
- POST /api/v1/temps/assign
|
||||
- POST /api/v1/temps/clear
|
||||
|
||||
## MVP Display Goals
|
||||
|
||||
@@ -120,10 +120,10 @@ Then interactive:
|
||||
|
||||
1. Dashboard boots.
|
||||
2. Connects to cargo ESP32 AP.
|
||||
3. Polls GET /status.
|
||||
3. Polls GET /api/v1/status.
|
||||
4. Shows disconnected state until valid JSON is received.
|
||||
5. Updates dashboard on a fixed interval.
|
||||
6. Relay buttons call POST /relay/set.
|
||||
6. Relay buttons call POST /api/v1/relay/set.
|
||||
|
||||
## Polling Guidance
|
||||
|
||||
|
||||
@@ -209,7 +209,6 @@ input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;ba
|
||||
</div>
|
||||
<div class="btnrow">
|
||||
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
||||
<button onclick="reconnectBms()">Reconnect BMS</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -218,7 +217,6 @@ input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;ba
|
||||
<div class="label">System Actions</div>
|
||||
<div class="sub">Use carefully. Factory reset clears install-specific configuration.</div>
|
||||
<div class="btnrow">
|
||||
<button onclick="restartController()">Restart</button>
|
||||
<button class="off" onclick="factoryReset()">Factory Reset</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -240,6 +238,8 @@ input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;ba
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE="/api/v1";
|
||||
function api(path){return API_BASE+path;}
|
||||
const $=id=>document.getElementById(id);
|
||||
function fmt(n,d=1){return typeof n==="number"?n.toFixed(d):"--"}
|
||||
function temp(v){return typeof v==="number"?fmt(v,1)+"°F":"Offline"}
|
||||
@@ -323,12 +323,12 @@ function showTab(name){
|
||||
});
|
||||
}
|
||||
async function setBmsEnabled(enabled){
|
||||
await fetch("/config/bms",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled})});
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await fetch(api("/config/bms"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled})});
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
}
|
||||
async function relay(id,on){
|
||||
await fetch("/relay/set",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,state:on})});
|
||||
await fetch(api("/relay/set"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,state:on})});
|
||||
await load();
|
||||
}
|
||||
function render(data){
|
||||
@@ -411,13 +411,13 @@ async function saveDeviceConfig(){
|
||||
const name=$("deviceNameInput")?.value.trim();
|
||||
if(!name){ alert("Device name is required"); return; }
|
||||
|
||||
await fetch("/config/device",{
|
||||
await fetch(api("/config/device"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({device_name:name})
|
||||
});
|
||||
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
alert("Device name saved");
|
||||
}
|
||||
@@ -432,7 +432,7 @@ async function saveBmsFullConfig(){
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch("/config/bms",{
|
||||
await fetch(api("/config/bms"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({
|
||||
@@ -443,22 +443,16 @@ async function saveBmsFullConfig(){
|
||||
})
|
||||
});
|
||||
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await reconnectBms(false);
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
alert("BMS config saved");
|
||||
}
|
||||
|
||||
async function reconnectBms(showAlert=true){
|
||||
await fetch("/bms/reconnect",{method:"POST"});
|
||||
if(showAlert) alert("BMS reconnect requested");
|
||||
}
|
||||
|
||||
async function scanTemps(){
|
||||
const box=$("tempScanResults");
|
||||
if(box) box.innerHTML=`<div class="item"><span>Scanning...</span><strong class="muted">wait</strong></div>`;
|
||||
|
||||
const r=await fetch("/temps/scan",{method:"POST"});
|
||||
const r=await fetch(api("/temps/scan"),{method:"POST"});
|
||||
const d=await r.json();
|
||||
const devices=d.devices||[];
|
||||
|
||||
@@ -487,13 +481,13 @@ async function scanTemps(){
|
||||
}
|
||||
|
||||
async function assignTemp(id,index){
|
||||
await fetch("/temps/assign",{
|
||||
await fetch(api("/temps/assign"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({id:id,index:index})
|
||||
});
|
||||
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
alert("Temp probe assigned to "+id);
|
||||
}
|
||||
@@ -501,7 +495,7 @@ async function assignTemp(id,index){
|
||||
async function clearTempAssignment(id){
|
||||
if(!confirm("Clear probe assignment for "+id+"?")) return;
|
||||
|
||||
const r=await fetch("/temps/clear",{
|
||||
const r=await fetch(api("/temps/clear"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({id:id})
|
||||
@@ -517,17 +511,11 @@ async function clearTempAssignment(id){
|
||||
alert("Temp probe assignment cleared for "+id);
|
||||
}
|
||||
|
||||
async function restartController(){
|
||||
if(!confirm("Restart the controller now?")) return;
|
||||
await fetch("/system/restart",{method:"POST"});
|
||||
alert("Restart requested");
|
||||
}
|
||||
|
||||
async function factoryReset(){
|
||||
if(!confirm("Factory reset all controller configuration?")) return;
|
||||
if(!confirm("This clears WiFi/BMS/relay/temp config. Continue?")) return;
|
||||
|
||||
await fetch("/config/factory-reset",{method:"POST"});
|
||||
await fetch(api("/config/factory-reset"),{method:"POST"});
|
||||
alert("Factory reset requested. Reconnect to the controller AP if needed.");
|
||||
}
|
||||
|
||||
@@ -574,7 +562,7 @@ function renderConfigControls(data){
|
||||
}
|
||||
|
||||
async function saveRelayConfig(){
|
||||
const r=await fetch("/status",{cache:"no-store"});
|
||||
const r=await fetch(api("/status"),{cache:"no-store"});
|
||||
const data=await r.json();
|
||||
const relays=data.config?.relays||[];
|
||||
|
||||
@@ -582,7 +570,7 @@ async function saveRelayConfig(){
|
||||
const name=$("relayName"+i)?.value.trim();
|
||||
if(!name) continue;
|
||||
|
||||
await fetch("/config/relay",{
|
||||
await fetch(api("/config/relay"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({
|
||||
@@ -593,13 +581,13 @@ async function saveRelayConfig(){
|
||||
});
|
||||
}
|
||||
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
alert("Relay names saved");
|
||||
}
|
||||
|
||||
async function saveTempConfig(){
|
||||
const r=await fetch("/status",{cache:"no-store"});
|
||||
const r=await fetch(api("/status"),{cache:"no-store"});
|
||||
const data=await r.json();
|
||||
const temps=data.config?.temperature_sensors||[];
|
||||
|
||||
@@ -613,7 +601,7 @@ async function saveTempConfig(){
|
||||
const enabled=$("tempEnabled"+i)?.checked || false;
|
||||
const weather=$("tempWeather"+i)?.checked || false;
|
||||
|
||||
await fetch("/config/temp",{
|
||||
await fetch(api("/config/temp"),{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({
|
||||
@@ -621,20 +609,19 @@ async function saveTempConfig(){
|
||||
name:name,
|
||||
address:temps[i].address||"",
|
||||
enabled:enabled,
|
||||
weather:weather,
|
||||
weather:weather
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await fetch(api("/config/save"),{method:"POST"});
|
||||
await load();
|
||||
alert("Temperature config saved");
|
||||
}
|
||||
|
||||
async function loadWifiConfig(){
|
||||
try{
|
||||
const r=await fetch("/config/wifi",{cache:"no-store"});
|
||||
const r=await fetch(api("/config/wifi"),{cache:"no-store"});
|
||||
const d=await r.json();
|
||||
const nets=d.wifi?.networks||[];
|
||||
for(let i=0;i<3;i++){
|
||||
@@ -652,17 +639,17 @@ async function saveWifi(){
|
||||
const priority=parseInt($("w"+i+"r").value||i,10);
|
||||
if(ssid) networks.push({ssid,password,priority});
|
||||
}
|
||||
await fetch("/config/wifi",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({networks})});
|
||||
await fetch(api("/config/wifi"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({networks})});
|
||||
await loadWifiConfig();
|
||||
await load();
|
||||
}
|
||||
async function connectWifi(){
|
||||
await fetch("/wifi/connect",{method:"POST"});
|
||||
await fetch(api("/wifi/connect"),{method:"POST"});
|
||||
await load();
|
||||
}
|
||||
async function load(){
|
||||
try{
|
||||
const r=await fetch("/status",{cache:"no-store"});
|
||||
const r=await fetch(api("/status"),{cache:"no-store"});
|
||||
render(await r.json());
|
||||
}catch(e){
|
||||
$("conn").textContent="Offline";
|
||||
@@ -678,6 +665,8 @@ setInterval(load,3000);
|
||||
)rawliteral";
|
||||
|
||||
WebServer server(80);
|
||||
|
||||
#define API_V1(path) "/api/v1" path
|
||||
HardwareSerial DashboardSerial(2);
|
||||
|
||||
String uartLineBuffer;
|
||||
@@ -2445,6 +2434,32 @@ void setup() {
|
||||
server.send_P(200, "text/html", INDEX_HTML);
|
||||
});
|
||||
|
||||
server.on(API_V1("/status"), handleStatus);
|
||||
server.on(API_V1("/relay/set"), HTTP_POST, handleSetRelayPost);
|
||||
|
||||
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
||||
server.on(API_V1("/config/wifi"), HTTP_GET, handleGetWifiConfig);
|
||||
server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig);
|
||||
server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect);
|
||||
server.on(API_V1("/wifi/clear"), HTTP_POST, handleWifiClear);
|
||||
|
||||
server.on(API_V1("/config/device"), HTTP_POST, handleUpdateDeviceConfig);
|
||||
server.on(API_V1("/config/relay"), HTTP_POST, handleUpdateRelayConfig);
|
||||
server.on(API_V1("/config/bms"), HTTP_POST, handleUpdateBmsConfig);
|
||||
server.on(API_V1("/config/temp"), HTTP_POST, handleUpdateTempSensorConfig);
|
||||
server.on(API_V1("/config/factory-reset"), HTTP_POST, handleFactoryResetConfig);
|
||||
server.on(API_V1("/config/save"), HTTP_POST, handleSaveConfig);
|
||||
|
||||
server.on(API_V1("/temps/scan"), HTTP_POST, handleTempScan);
|
||||
server.on(API_V1("/temps/assign"), HTTP_POST, handleTempAssign);
|
||||
server.on(API_V1("/temps/clear"), HTTP_POST, handleTempClear);
|
||||
|
||||
server.on(API_V1("/bms/setup/enter"), HTTP_POST, handleEnterBmsSetup);
|
||||
server.on(API_V1("/bms/setup/exit"), HTTP_POST, handleExitBmsSetup);
|
||||
server.on(API_V1("/bms/scan"), HTTP_POST, handleBleScan);
|
||||
server.on(API_V1("/bms/select"), HTTP_POST, handleSelectBms);
|
||||
|
||||
// Compatibility aliases for pre-versioned local clients.
|
||||
server.on("/status", handleStatus);
|
||||
server.on("/relay/set", HTTP_POST, handleSetRelayPost);
|
||||
server.on("/relay/relay_1/on", HTTP_GET, handleGenericRelayRoute);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import json
|
||||
|
||||
|
||||
class HttpClient:
|
||||
def __init__(self, requests_module, base_url="http://192.168.4.1"):
|
||||
self.requests = requests_module
|
||||
self.base_url = base_url.rstrip("/")
|
||||
if not self.base_url.endswith("/api/v1"):
|
||||
self.base_url = f"{self.base_url}/api/v1"
|
||||
|
||||
def get_status(self):
|
||||
response = self.requests.get(f"{self.base_url}/status")
|
||||
return response.json()
|
||||
|
||||
def set_relay(self, relay, enabled):
|
||||
state = "on" if enabled else "off"
|
||||
response = self.requests.get(f"{self.base_url}/relay/{relay}/{state}")
|
||||
response = self.requests.post(
|
||||
f"{self.base_url}/relay/set",
|
||||
json={"id": relay, "state": enabled},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
@@ -24,6 +24,8 @@ class AppState:
|
||||
}
|
||||
|
||||
self.relays = {
|
||||
"relay_1": False,
|
||||
"relay_2": False,
|
||||
"starlink": False,
|
||||
"fridge": False,
|
||||
}
|
||||
@@ -51,8 +53,8 @@ class AppState:
|
||||
self.network.update(message.get("network", {}))
|
||||
|
||||
def update_from_relay_response(self, message):
|
||||
relay = message.get("relay")
|
||||
enabled = message.get("enabled")
|
||||
relay = message.get("relay", message.get("id"))
|
||||
enabled = message.get("enabled", message.get("state"))
|
||||
|
||||
if relay in self.relays and enabled is not None:
|
||||
self.relays[relay] = bool(enabled)
|
||||
|
||||
@@ -22,14 +22,41 @@ def assert_keys(payload, keys):
|
||||
|
||||
|
||||
def registered_routes():
|
||||
pattern = re.compile(r'server\.on\("([^"]+)"(?:,\s*(HTTP_[A-Z]+))?')
|
||||
return {(match.group(1), match.group(2) or "ANY") for match in pattern.finditer(firmware_source())}
|
||||
source = firmware_source()
|
||||
literal_pattern = re.compile(r'server\.on\("([^"]+)"(?:,\s*(HTTP_[A-Z]+))?')
|
||||
macro_pattern = re.compile(r'server\.on\(API_V1\("([^"]+)"\)(?:,\s*(HTTP_[A-Z]+))?')
|
||||
|
||||
routes = {(match.group(1), match.group(2) or "ANY") for match in literal_pattern.finditer(source)}
|
||||
routes.update({
|
||||
(f"/api/v1{match.group(1)}", match.group(2) or "ANY")
|
||||
for match in macro_pattern.finditer(source)
|
||||
})
|
||||
return routes
|
||||
|
||||
|
||||
def test_firmware_registers_current_http_contract_routes():
|
||||
routes = registered_routes()
|
||||
|
||||
expected_routes = {
|
||||
("/api/v1/status", "ANY"),
|
||||
("/api/v1/config", "HTTP_GET"),
|
||||
("/api/v1/relay/set", "HTTP_POST"),
|
||||
("/api/v1/config/wifi", "HTTP_GET"),
|
||||
("/api/v1/config/wifi", "HTTP_POST"),
|
||||
("/api/v1/wifi/connect", "HTTP_POST"),
|
||||
("/api/v1/wifi/clear", "HTTP_POST"),
|
||||
("/api/v1/temps/scan", "HTTP_POST"),
|
||||
("/api/v1/temps/assign", "HTTP_POST"),
|
||||
("/api/v1/temps/clear", "HTTP_POST"),
|
||||
}
|
||||
|
||||
assert expected_routes <= routes
|
||||
|
||||
|
||||
def test_firmware_keeps_root_compatibility_aliases():
|
||||
routes = registered_routes()
|
||||
|
||||
compatibility_routes = {
|
||||
("/status", "ANY"),
|
||||
("/config", "HTTP_GET"),
|
||||
("/relay/set", "HTTP_POST"),
|
||||
@@ -42,7 +69,21 @@ def test_firmware_registers_current_http_contract_routes():
|
||||
("/temps/clear", "HTTP_POST"),
|
||||
}
|
||||
|
||||
assert expected_routes <= routes
|
||||
assert compatibility_routes <= routes
|
||||
|
||||
|
||||
def test_embedded_webui_uses_versioned_api_routes():
|
||||
source = firmware_source()
|
||||
|
||||
assert 'const API_BASE="/api/v1";' in source
|
||||
assert 'fetch(api("' in source
|
||||
assert 'fetch("/status"' not in source
|
||||
assert 'fetch("/config' not in source
|
||||
assert 'fetch("/relay' not in source
|
||||
assert 'fetch("/temps' not in source
|
||||
assert 'fetch("/wifi' not in source
|
||||
assert "/bms/reconnect" not in source
|
||||
assert "/system/restart" not in source
|
||||
|
||||
|
||||
def test_status_fixture_matches_dashboard_contract_shape():
|
||||
|
||||
+33
-11
@@ -241,6 +241,7 @@ class FakeResponse:
|
||||
class FakeRequests:
|
||||
def __init__(self):
|
||||
self.urls = []
|
||||
self.posts = []
|
||||
self.responses = []
|
||||
|
||||
def get(self, url):
|
||||
@@ -251,6 +252,15 @@ class FakeRequests:
|
||||
|
||||
return FakeResponse({"ok": True})
|
||||
|
||||
def post(self, url, json=None):
|
||||
self.urls.append(url)
|
||||
self.posts.append({"url": url, "json": json})
|
||||
|
||||
if self.responses:
|
||||
return FakeResponse(self.responses.pop(0))
|
||||
|
||||
return FakeResponse({"ok": True})
|
||||
|
||||
|
||||
def test_http_client_get_status():
|
||||
from comms.http_client import HttpClient
|
||||
@@ -264,7 +274,7 @@ def test_http_client_get_status():
|
||||
|
||||
payload = client.get_status()
|
||||
|
||||
assert fake_requests.urls == ["http://192.168.4.1/status"]
|
||||
assert fake_requests.urls == ["http://192.168.4.1/api/v1/status"]
|
||||
assert payload["type"] == "status_response"
|
||||
assert payload["battery"]["soc"] == 82
|
||||
|
||||
@@ -275,9 +285,13 @@ def test_http_client_set_relay_on():
|
||||
fake_requests = FakeRequests()
|
||||
client = HttpClient(fake_requests)
|
||||
|
||||
payload = client.set_relay("starlink", True)
|
||||
payload = client.set_relay("relay_1", True)
|
||||
|
||||
assert fake_requests.urls == ["http://192.168.4.1/relay/starlink/on"]
|
||||
assert fake_requests.urls == ["http://192.168.4.1/api/v1/relay/set"]
|
||||
assert fake_requests.posts == [{
|
||||
"url": "http://192.168.4.1/api/v1/relay/set",
|
||||
"json": {"id": "relay_1", "state": True},
|
||||
}]
|
||||
assert payload == {"ok": True}
|
||||
|
||||
|
||||
@@ -287,9 +301,13 @@ def test_http_client_set_relay_off():
|
||||
fake_requests = FakeRequests()
|
||||
client = HttpClient(fake_requests)
|
||||
|
||||
payload = client.set_relay("fridge", False)
|
||||
payload = client.set_relay("relay_2", False)
|
||||
|
||||
assert fake_requests.urls == ["http://192.168.4.1/relay/fridge/off"]
|
||||
assert fake_requests.urls == ["http://192.168.4.1/api/v1/relay/set"]
|
||||
assert fake_requests.posts == [{
|
||||
"url": "http://192.168.4.1/api/v1/relay/set",
|
||||
"json": {"id": "relay_2", "state": False},
|
||||
}]
|
||||
assert payload == {"ok": True}
|
||||
|
||||
|
||||
@@ -317,7 +335,7 @@ def test_communication_service_http_fallback_status():
|
||||
assert response["type"] == "status_response"
|
||||
assert state.battery["soc"] == 66
|
||||
assert fake_uart.writes == []
|
||||
assert fake_requests.urls == ["http://192.168.4.1/status"]
|
||||
assert fake_requests.urls == ["http://192.168.4.1/api/v1/status"]
|
||||
|
||||
|
||||
def test_communication_service_http_fallback_relay():
|
||||
@@ -330,8 +348,8 @@ def test_communication_service_http_fallback_relay():
|
||||
fake_requests.responses = [
|
||||
{
|
||||
"type": "relay_response",
|
||||
"relay": "fridge",
|
||||
"enabled": True,
|
||||
"id": "relay_1",
|
||||
"state": True,
|
||||
"ok": True,
|
||||
}
|
||||
]
|
||||
@@ -344,12 +362,16 @@ def test_communication_service_http_fallback_relay():
|
||||
)
|
||||
|
||||
service.enable_http_fallback()
|
||||
response = service.set_relay("fridge", True)
|
||||
response = service.set_relay("relay_1", True)
|
||||
|
||||
assert response["type"] == "relay_response"
|
||||
assert state.relays["fridge"] is True
|
||||
assert state.relays["relay_1"] is True
|
||||
assert fake_uart.writes == []
|
||||
assert fake_requests.urls == ["http://192.168.4.1/relay/fridge/on"]
|
||||
assert fake_requests.urls == ["http://192.168.4.1/api/v1/relay/set"]
|
||||
assert fake_requests.posts == [{
|
||||
"url": "http://192.168.4.1/api/v1/relay/set",
|
||||
"json": {"id": "relay_1", "state": True},
|
||||
}]
|
||||
|
||||
|
||||
def test_communication_service_marks_uart_connected_on_status():
|
||||
|
||||
Reference in New Issue
Block a user