Add maintenance API endpoints
This commit is contained in:
+129
-3
@@ -26,11 +26,15 @@ Current firmware version:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
GET /
|
GET /
|
||||||
|
GET /api/v1/health
|
||||||
|
GET /api/v1/capabilities
|
||||||
GET /api/v1/status
|
GET /api/v1/status
|
||||||
GET /api/v1/config
|
GET /api/v1/config
|
||||||
|
GET /api/v1/config/export
|
||||||
|
|
||||||
POST /api/v1/relay/set
|
POST /api/v1/relay/set
|
||||||
|
|
||||||
|
POST /api/v1/config/import
|
||||||
POST /api/v1/config/device
|
POST /api/v1/config/device
|
||||||
POST /api/v1/config/relay
|
POST /api/v1/config/relay
|
||||||
POST /api/v1/config/temp
|
POST /api/v1/config/temp
|
||||||
@@ -75,6 +79,70 @@ 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.
|
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.
|
||||||
|
|
||||||
|
## Health
|
||||||
|
|
||||||
|
### GET /api/v1/health
|
||||||
|
|
||||||
|
Returns a lightweight liveness response for dashboards and service tools.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "health_response",
|
||||||
|
"ok": true,
|
||||||
|
"api_version": "v1",
|
||||||
|
"firmware_name": "overland-controller",
|
||||||
|
"firmware_version": "0.4.0",
|
||||||
|
"uptime_seconds": 123,
|
||||||
|
"network": {
|
||||||
|
"ap_enabled": true,
|
||||||
|
"ap_ip": "192.168.4.1",
|
||||||
|
"sta_enabled": true,
|
||||||
|
"sta_connected": false,
|
||||||
|
"sta_ssid": "",
|
||||||
|
"sta_ip": ""
|
||||||
|
},
|
||||||
|
"bms": {
|
||||||
|
"configured": true,
|
||||||
|
"connected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### GET /api/v1/capabilities
|
||||||
|
|
||||||
|
Returns API and feature discovery data for clients.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "capabilities_response",
|
||||||
|
"ok": true,
|
||||||
|
"api_version": "v1",
|
||||||
|
"firmware_name": "overland-controller",
|
||||||
|
"firmware_version": "0.4.0",
|
||||||
|
"endpoints": [
|
||||||
|
"GET /api/v1/status",
|
||||||
|
"POST /api/v1/relay/set"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"relay_count": 2,
|
||||||
|
"temperature_sensor_count": 8,
|
||||||
|
"runtime_temperature_status_count": 4,
|
||||||
|
"wifi_network_count": 3
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"relay_control": true,
|
||||||
|
"temperature_scan": true,
|
||||||
|
"wifi_config": true,
|
||||||
|
"config_backup_restore": true,
|
||||||
|
"bms_setup": true,
|
||||||
|
"uart_json": true,
|
||||||
|
"root_compatibility_aliases": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
### GET /api/v1/status
|
### GET /api/v1/status
|
||||||
@@ -261,19 +329,19 @@ Returns saved controller configuration.
|
|||||||
"address": "AA:BB:CC:DD:EE:FF",
|
"address": "AA:BB:CC:DD:EE:FF",
|
||||||
"address_type": "public"
|
"address_type": "public"
|
||||||
},
|
},
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
"temperature_sensors": [
|
"temperature_sensors": [
|
||||||
{
|
{
|
||||||
"id": "temp_1",
|
"id": "temp_1",
|
||||||
"name": "Cabin",
|
"name": "Cabin",
|
||||||
"address": "28:AA:BB:CC:DD:EE:FF:00",
|
"address": "28:AA:BB:CC:DD:EE:FF:00",
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"weather": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: current `GET /api/v1/config` omits the `weather` field, while `/api/v1/status.config.temperature_sensors[]` includes it.
|
|
||||||
|
|
||||||
### POST /api/v1/config/device
|
### POST /api/v1/config/device
|
||||||
|
|
||||||
Updates the controller display name.
|
Updates the controller display name.
|
||||||
@@ -368,6 +436,63 @@ Clears saved configuration and restores firmware defaults.
|
|||||||
|
|
||||||
Returns the reset config.
|
Returns the reset config.
|
||||||
|
|
||||||
|
### GET /api/v1/config/export
|
||||||
|
|
||||||
|
Returns a backup envelope containing controller config and saved WiFi networks.
|
||||||
|
|
||||||
|
Important: export includes saved WiFi passwords so the backup can be restored. Treat the response as sensitive.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "config_export_response",
|
||||||
|
"ok": true,
|
||||||
|
"api_version": "v1",
|
||||||
|
"firmware_name": "overland-controller",
|
||||||
|
"firmware_version": "0.4.0",
|
||||||
|
"config": {
|
||||||
|
"device_name": "Overland Controller",
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
|
"relays": [],
|
||||||
|
"bms": {},
|
||||||
|
"temperature_sensors": []
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"ssid": "Starlink",
|
||||||
|
"priority": 1,
|
||||||
|
"password_set": true,
|
||||||
|
"password": "password_here"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/config/import
|
||||||
|
|
||||||
|
Restores a config backup. The preferred request shape is the same envelope returned by `GET /api/v1/config/export`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"device_name": "Overland Controller",
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
|
"relays": [],
|
||||||
|
"bms": {},
|
||||||
|
"temperature_sensors": []
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"networks": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The endpoint also accepts the bare config object without an outer `config` field. When `wifi.networks` is included, saved STA WiFi networks are replaced and persisted. It does not initiate STA reconnect; use `POST /api/v1/wifi/connect` after import if needed.
|
||||||
|
|
||||||
|
Returns the updated config.
|
||||||
|
|
||||||
## Relay Control
|
## Relay Control
|
||||||
|
|
||||||
### POST /api/v1/relay/set
|
### POST /api/v1/relay/set
|
||||||
@@ -671,6 +796,7 @@ Known error codes:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
invalid_json
|
invalid_json
|
||||||
|
invalid_config
|
||||||
unknown_relay
|
unknown_relay
|
||||||
unknown_temp_sensor
|
unknown_temp_sensor
|
||||||
invalid_temp_selection
|
invalid_temp_selection
|
||||||
|
|||||||
@@ -93,8 +93,12 @@ HTTP is the primary integration contract for:
|
|||||||
|
|
||||||
Preferred direction:
|
Preferred direction:
|
||||||
|
|
||||||
|
GET /api/v1/health
|
||||||
|
GET /api/v1/capabilities
|
||||||
GET /api/v1/status
|
GET /api/v1/status
|
||||||
GET /api/v1/config
|
GET /api/v1/config
|
||||||
|
GET /api/v1/config/export
|
||||||
|
POST /api/v1/config/import
|
||||||
POST /api/v1/relay/set
|
POST /api/v1/relay/set
|
||||||
|
|
||||||
Pre-versioned root routes remain registered as compatibility aliases for existing local clients.
|
Pre-versioned root routes remain registered as compatibility aliases for existing local clients.
|
||||||
|
|||||||
@@ -166,6 +166,19 @@ After factory reset:
|
|||||||
- Temperature names return to generic defaults
|
- Temperature names return to generic defaults
|
||||||
- BMS is unconfigured until selected or manually entered
|
- BMS is unconfigured until selected or manually entered
|
||||||
|
|
||||||
|
## Backup And Restore
|
||||||
|
|
||||||
|
Configuration backup includes controller config and saved STA WiFi networks.
|
||||||
|
|
||||||
|
HTTP endpoints:
|
||||||
|
|
||||||
|
GET /api/v1/config/export
|
||||||
|
POST /api/v1/config/import
|
||||||
|
|
||||||
|
`config/export` includes saved WiFi passwords so the backup can be restored. Treat exported JSON as sensitive.
|
||||||
|
|
||||||
|
`config/import` persists the restored config. If WiFi networks are imported, run `POST /api/v1/wifi/connect` afterward to attempt STA connection.
|
||||||
|
|
||||||
## Configuration Persistence
|
## Configuration Persistence
|
||||||
|
|
||||||
Configuration is stored using ESP32 non-volatile storage.
|
Configuration is stored using ESP32 non-volatile storage.
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ Control surfaces:
|
|||||||
|
|
||||||
| Feature | USB Serial | UART JSON | HTTP API | Notes |
|
| Feature | USB Serial | UART JSON | HTTP API | Notes |
|
||||||
|---|---:|---:|---:|---|
|
|---|---:|---:|---:|---|
|
||||||
|
| Health | No | No | Yes | `/api/v1/health` is lightweight HTTP liveness |
|
||||||
|
| Capabilities | No | No | Yes | `/api/v1/capabilities` supports client discovery |
|
||||||
| Status | Yes | Yes | Yes | `/api/v1/status` equals `status_request` |
|
| Status | Yes | Yes | Yes | `/api/v1/status` equals `status_request` |
|
||||||
| Config view | Yes | Yes | Yes | `/api/v1/config` equals `config_request` |
|
| Config view | Yes | Yes | Yes | `/api/v1/config` equals `config_request` |
|
||||||
|
| Config export | No | No | Yes | `/api/v1/config/export` includes WiFi passwords |
|
||||||
|
| Config import | No | No | Yes | `/api/v1/config/import` persists restored config |
|
||||||
| Relay control | Yes | Yes | Yes | Preferred HTTP endpoint is `POST /api/v1/relay/set` |
|
| 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` |
|
| 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` |
|
| Relay config | Yes | Yes | Yes | `relayname`, `config_relay`, `/api/v1/config/relay` |
|
||||||
|
|||||||
@@ -47,11 +47,15 @@ All grounds remain common.
|
|||||||
## API endpoints consumed
|
## API endpoints consumed
|
||||||
|
|
||||||
Minimum dashboard MVP:
|
Minimum dashboard MVP:
|
||||||
|
- GET /api/v1/health
|
||||||
- GET /api/v1/status
|
- GET /api/v1/status
|
||||||
- POST /api/v1/relay/set
|
- POST /api/v1/relay/set
|
||||||
|
|
||||||
Potential later use:
|
Potential later use:
|
||||||
|
- GET /api/v1/capabilities
|
||||||
- GET /api/v1/config
|
- GET /api/v1/config
|
||||||
|
- GET /api/v1/config/export
|
||||||
|
- POST /api/v1/config/import
|
||||||
- POST /api/v1/config/save
|
- POST /api/v1/config/save
|
||||||
- POST /api/v1/temps/scan
|
- POST /api/v1/temps/scan
|
||||||
- POST /api/v1/temps/assign
|
- POST /api/v1/temps/assign
|
||||||
@@ -120,10 +124,11 @@ Then interactive:
|
|||||||
|
|
||||||
1. Dashboard boots.
|
1. Dashboard boots.
|
||||||
2. Connects to cargo ESP32 AP.
|
2. Connects to cargo ESP32 AP.
|
||||||
3. Polls GET /api/v1/status.
|
3. Checks GET /api/v1/health.
|
||||||
4. Shows disconnected state until valid JSON is received.
|
4. Polls GET /api/v1/status.
|
||||||
5. Updates dashboard on a fixed interval.
|
5. Shows disconnected state until valid JSON is received.
|
||||||
6. Relay buttons call POST /api/v1/relay/set.
|
6. Updates dashboard on a fixed interval.
|
||||||
|
7. Relay buttons call POST /api/v1/relay/set.
|
||||||
|
|
||||||
## Polling Guidance
|
## Polling Guidance
|
||||||
|
|
||||||
|
|||||||
@@ -861,6 +861,240 @@ void printNetworkStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void buildConfigDocument(JsonObject doc) {
|
||||||
|
doc["device_name"] = appConfig.deviceName;
|
||||||
|
|
||||||
|
JsonArray relaysArray = doc.createNestedArray("relays");
|
||||||
|
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||||
|
JsonObject relay = relaysArray.createNestedObject();
|
||||||
|
relay["id"] = appConfig.relays[i].id;
|
||||||
|
relay["name"] = appConfig.relays[i].name;
|
||||||
|
relay["pin"] = appConfig.relays[i].pin;
|
||||||
|
relay["enabled"] = appConfig.relays[i].enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject bms = doc.createNestedObject("bms");
|
||||||
|
bms["enabled"] = appConfig.bms.enabled;
|
||||||
|
bms["name"] = appConfig.bms.name;
|
||||||
|
bms["address"] = appConfig.bms.address;
|
||||||
|
bms["address_type"] = appConfig.bms.addressType;
|
||||||
|
|
||||||
|
doc["temperature_sensor_count"] = appConfig.tempSensorCount;
|
||||||
|
|
||||||
|
JsonArray temps = doc.createNestedArray("temperature_sensors");
|
||||||
|
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||||
|
JsonObject temp = temps.createNestedObject();
|
||||||
|
temp["id"] = appConfig.tempSensors[i].id;
|
||||||
|
temp["name"] = appConfig.tempSensors[i].name;
|
||||||
|
temp["address"] = appConfig.tempSensors[i].address;
|
||||||
|
temp["enabled"] = appConfig.tempSensors[i].enabled;
|
||||||
|
temp["weather"] = appConfig.tempSensors[i].weather;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildWifiConfigDocument(JsonObject wifi, bool includePasswords) {
|
||||||
|
wifi["ap_enabled"] = true;
|
||||||
|
wifi["sta_enabled"] = wifiNetworkCount > 0;
|
||||||
|
wifi["network_count"] = wifiNetworkCount;
|
||||||
|
wifi["active_ssid"] = activeStaSsid;
|
||||||
|
wifi["sta_connected"] = WiFi.status() == WL_CONNECTED;
|
||||||
|
wifi["ap_ip"] = WiFi.softAPIP().toString();
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
wifi["sta_ip"] = WiFi.localIP().toString();
|
||||||
|
} else {
|
||||||
|
wifi["sta_ip"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray networks = wifi.createNestedArray("networks");
|
||||||
|
for (int i = 0; i < wifiNetworkCount; i++) {
|
||||||
|
JsonObject network = networks.createNestedObject();
|
||||||
|
network["index"] = i + 1;
|
||||||
|
network["ssid"] = staSsids[i];
|
||||||
|
network["priority"] = staPriorities[i];
|
||||||
|
network["password_set"] = staPasswords[i].length() > 0;
|
||||||
|
if (includePasswords) {
|
||||||
|
network["password"] = staPasswords[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyWifiNetworks(JsonArray networks) {
|
||||||
|
wifiNetworkCount = 0;
|
||||||
|
|
||||||
|
for (JsonObject network : networks) {
|
||||||
|
if (wifiNetworkCount >= MAX_WIFI_NETWORKS) break;
|
||||||
|
|
||||||
|
String ssid = network["ssid"] | "";
|
||||||
|
String password = network["password"] | "";
|
||||||
|
|
||||||
|
ssid.trim();
|
||||||
|
password.trim();
|
||||||
|
|
||||||
|
if (ssid.length() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length() == 0) {
|
||||||
|
for (int oldIndex = 0; oldIndex < MAX_WIFI_NETWORKS; oldIndex++) {
|
||||||
|
if (staSsids[oldIndex] == ssid && staPasswords[oldIndex].length() > 0) {
|
||||||
|
password = staPasswords[oldIndex];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int priority = network["priority"] | (wifiNetworkCount + 1);
|
||||||
|
if (priority <= 0) priority = wifiNetworkCount + 1;
|
||||||
|
|
||||||
|
staSsids[wifiNetworkCount] = ssid;
|
||||||
|
staPasswords[wifiNetworkCount] = password;
|
||||||
|
staPriorities[wifiNetworkCount] = priority;
|
||||||
|
wifiNetworkCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = wifiNetworkCount; i < MAX_WIFI_NETWORKS; i++) {
|
||||||
|
staSsids[i] = "";
|
||||||
|
staPasswords[i] = "";
|
||||||
|
staPriorities[i] = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int findRelayConfigIndex(const String& id);
|
||||||
|
int findTempSensorConfigIndex(const String& id);
|
||||||
|
|
||||||
|
bool validateConfigImport(JsonObject doc, const char*& errorCode) {
|
||||||
|
if (doc["device_name"].isNull() || !doc["device_name"].is<String>()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["relays"].isNull() && !doc["relays"].is<JsonArray>()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["temperature_sensors"].isNull() && !doc["temperature_sensors"].is<JsonArray>()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["bms"].isNull() && !doc["bms"].is<JsonObject>()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["wifi"].isNull() && !doc["wifi"].is<JsonObject>()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc["temperature_sensor_count"].is<int>()) {
|
||||||
|
int count = doc["temperature_sensor_count"].as<int>();
|
||||||
|
if (count < 0 || count > MAX_TEMP_SENSORS) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray relays = doc["relays"].as<JsonArray>();
|
||||||
|
if (!relays.isNull()) {
|
||||||
|
for (JsonObject relay : relays) {
|
||||||
|
String id = relay["id"] | "";
|
||||||
|
if (findRelayConfigIndex(id) < 0) {
|
||||||
|
errorCode = "unknown_relay";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray temps = doc["temperature_sensors"].as<JsonArray>();
|
||||||
|
if (!temps.isNull()) {
|
||||||
|
for (JsonObject temp : temps) {
|
||||||
|
String id = temp["id"] | "";
|
||||||
|
if (findTempSensorConfigIndex(id) < 0) {
|
||||||
|
errorCode = "unknown_temp_sensor";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject bms = doc["bms"].as<JsonObject>();
|
||||||
|
if (!bms.isNull() && bms["address_type"].is<String>()) {
|
||||||
|
String addressType = bms["address_type"].as<String>();
|
||||||
|
addressType.toLowerCase();
|
||||||
|
if (addressType != "public" && addressType != "random") {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject wifi = doc["wifi"].as<JsonObject>();
|
||||||
|
if (!wifi.isNull()) {
|
||||||
|
JsonArray networks = wifi["networks"].as<JsonArray>();
|
||||||
|
if (networks.isNull()) {
|
||||||
|
errorCode = "invalid_config";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyImportedConfig(JsonObject doc) {
|
||||||
|
if (doc["device_name"].is<String>()) {
|
||||||
|
appConfig.deviceName = doc["device_name"].as<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc["temperature_sensor_count"].is<int>()) {
|
||||||
|
appConfig.tempSensorCount = doc["temperature_sensor_count"].as<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray relays = doc["relays"].as<JsonArray>();
|
||||||
|
if (!relays.isNull()) {
|
||||||
|
for (JsonObject relay : relays) {
|
||||||
|
int index = findRelayConfigIndex(relay["id"] | "");
|
||||||
|
if (index < 0) continue;
|
||||||
|
|
||||||
|
if (relay["name"].is<String>()) appConfig.relays[index].name = relay["name"].as<String>();
|
||||||
|
if (relay["enabled"].is<bool>()) appConfig.relays[index].enabled = relay["enabled"].as<bool>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject bms = doc["bms"].as<JsonObject>();
|
||||||
|
if (!bms.isNull()) {
|
||||||
|
if (bms["enabled"].is<bool>()) appConfig.bms.enabled = bms["enabled"].as<bool>();
|
||||||
|
if (bms["name"].is<String>()) appConfig.bms.name = bms["name"].as<String>();
|
||||||
|
if (bms["address"].is<String>()) appConfig.bms.address = bms["address"].as<String>();
|
||||||
|
if (bms["address_type"].is<String>()) {
|
||||||
|
appConfig.bms.addressType = bms["address_type"].as<String>();
|
||||||
|
appConfig.bms.addressType.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray temps = doc["temperature_sensors"].as<JsonArray>();
|
||||||
|
if (!temps.isNull()) {
|
||||||
|
for (JsonObject temp : temps) {
|
||||||
|
int index = findTempSensorConfigIndex(temp["id"] | "");
|
||||||
|
if (index < 0) continue;
|
||||||
|
|
||||||
|
if (temp["name"].is<String>()) appConfig.tempSensors[index].name = temp["name"].as<String>();
|
||||||
|
if (temp["address"].is<String>()) appConfig.tempSensors[index].address = temp["address"].as<String>();
|
||||||
|
if (temp["enabled"].is<bool>()) appConfig.tempSensors[index].enabled = temp["enabled"].as<bool>();
|
||||||
|
if (temp["weather"].is<bool>()) appConfig.tempSensors[index].weather = temp["weather"].as<bool>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject wifi = doc["wifi"].as<JsonObject>();
|
||||||
|
if (!wifi.isNull()) {
|
||||||
|
JsonArray networks = wifi["networks"].as<JsonArray>();
|
||||||
|
applyWifiNetworks(networks);
|
||||||
|
saveWifiConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
float calculateRuntimeHours() {
|
float calculateRuntimeHours() {
|
||||||
if (!bmsData.valid || bmsData.current >= 0) return 0;
|
if (!bmsData.valid || bmsData.current >= 0) return 0;
|
||||||
@@ -997,32 +1231,7 @@ void buildStatusDocument(JsonDocument& doc) {
|
|||||||
system["uptime_seconds"] = millis() / 1000;
|
system["uptime_seconds"] = millis() / 1000;
|
||||||
|
|
||||||
JsonObject configObj = doc.createNestedObject("config");
|
JsonObject configObj = doc.createNestedObject("config");
|
||||||
configObj["device_name"] = appConfig.deviceName;
|
buildConfigDocument(configObj);
|
||||||
|
|
||||||
JsonArray configRelays = configObj.createNestedArray("relays");
|
|
||||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
|
||||||
JsonObject relayConfig = configRelays.createNestedObject();
|
|
||||||
relayConfig["id"] = appConfig.relays[i].id;
|
|
||||||
relayConfig["name"] = appConfig.relays[i].name;
|
|
||||||
relayConfig["pin"] = appConfig.relays[i].pin;
|
|
||||||
relayConfig["enabled"] = appConfig.relays[i].enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject bmsConfig = configObj.createNestedObject("bms");
|
|
||||||
bmsConfig["enabled"] = appConfig.bms.enabled;
|
|
||||||
bmsConfig["name"] = appConfig.bms.name;
|
|
||||||
bmsConfig["address"] = appConfig.bms.address;
|
|
||||||
bmsConfig["address_type"] = appConfig.bms.addressType;
|
|
||||||
|
|
||||||
JsonArray tempConfigs = configObj.createNestedArray("temperature_sensors");
|
|
||||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
|
||||||
JsonObject tempConfig = tempConfigs.createNestedObject();
|
|
||||||
tempConfig["id"] = appConfig.tempSensors[i].id;
|
|
||||||
tempConfig["name"] = appConfig.tempSensors[i].name;
|
|
||||||
tempConfig["address"] = appConfig.tempSensors[i].address;
|
|
||||||
tempConfig["enabled"] = appConfig.tempSensors[i].enabled;
|
|
||||||
tempConfig["weather"] = appConfig.tempSensors[i].weather;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendStatus(Stream& output, bool pretty = false) {
|
void sendStatus(Stream& output, bool pretty = false) {
|
||||||
@@ -1850,32 +2059,8 @@ void handleDebugSerial() {
|
|||||||
|
|
||||||
void sendConfigJson() {
|
void sendConfigJson() {
|
||||||
DynamicJsonDocument doc(4096);
|
DynamicJsonDocument doc(4096);
|
||||||
|
JsonObject root = doc.to<JsonObject>();
|
||||||
doc["device_name"] = appConfig.deviceName;
|
buildConfigDocument(root);
|
||||||
|
|
||||||
JsonArray relaysArray = doc.createNestedArray("relays");
|
|
||||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
|
||||||
JsonObject relay = relaysArray.createNestedObject();
|
|
||||||
relay["id"] = appConfig.relays[i].id;
|
|
||||||
relay["name"] = appConfig.relays[i].name;
|
|
||||||
relay["pin"] = appConfig.relays[i].pin;
|
|
||||||
relay["enabled"] = appConfig.relays[i].enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject bms = doc.createNestedObject("bms");
|
|
||||||
bms["enabled"] = appConfig.bms.enabled;
|
|
||||||
bms["name"] = appConfig.bms.name;
|
|
||||||
bms["address"] = appConfig.bms.address;
|
|
||||||
bms["address_type"] = appConfig.bms.addressType;
|
|
||||||
|
|
||||||
JsonArray temps = doc.createNestedArray("temperature_sensors");
|
|
||||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
|
||||||
JsonObject temp = temps.createNestedObject();
|
|
||||||
temp["id"] = appConfig.tempSensors[i].id;
|
|
||||||
temp["name"] = appConfig.tempSensors[i].name;
|
|
||||||
temp["address"] = appConfig.tempSensors[i].address;
|
|
||||||
temp["enabled"] = appConfig.tempSensors[i].enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
String output;
|
String output;
|
||||||
serializeJson(doc, output);
|
serializeJson(doc, output);
|
||||||
@@ -1905,6 +2090,131 @@ void handleGetConfig() {
|
|||||||
sendConfigJson();
|
sendConfigJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleHealth() {
|
||||||
|
DynamicJsonDocument doc(1024);
|
||||||
|
|
||||||
|
doc["type"] = "health_response";
|
||||||
|
doc["ok"] = true;
|
||||||
|
doc["api_version"] = "v1";
|
||||||
|
doc["firmware_name"] = FIRMWARE_NAME;
|
||||||
|
doc["firmware_version"] = FIRMWARE_VERSION;
|
||||||
|
doc["uptime_seconds"] = millis() / 1000;
|
||||||
|
|
||||||
|
JsonObject network = doc.createNestedObject("network");
|
||||||
|
network["ap_enabled"] = true;
|
||||||
|
network["ap_ip"] = WiFi.softAPIP().toString();
|
||||||
|
network["sta_enabled"] = wifiNetworkCount > 0;
|
||||||
|
network["sta_connected"] = WiFi.status() == WL_CONNECTED;
|
||||||
|
network["sta_ssid"] = activeStaSsid;
|
||||||
|
network["sta_ip"] = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "";
|
||||||
|
|
||||||
|
JsonObject bms = doc.createNestedObject("bms");
|
||||||
|
bms["configured"] = appConfig.bms.enabled && appConfig.bms.address.length() > 0;
|
||||||
|
bms["connected"] = bmsData.connected;
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
server.send(200, "application/json", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCapabilities() {
|
||||||
|
DynamicJsonDocument doc(2048);
|
||||||
|
|
||||||
|
doc["type"] = "capabilities_response";
|
||||||
|
doc["ok"] = true;
|
||||||
|
doc["api_version"] = "v1";
|
||||||
|
doc["firmware_name"] = FIRMWARE_NAME;
|
||||||
|
doc["firmware_version"] = FIRMWARE_VERSION;
|
||||||
|
|
||||||
|
JsonArray endpoints = doc.createNestedArray("endpoints");
|
||||||
|
endpoints.add("GET /api/v1/health");
|
||||||
|
endpoints.add("GET /api/v1/capabilities");
|
||||||
|
endpoints.add("GET /api/v1/status");
|
||||||
|
endpoints.add("GET /api/v1/config");
|
||||||
|
endpoints.add("GET /api/v1/config/export");
|
||||||
|
endpoints.add("POST /api/v1/config/import");
|
||||||
|
endpoints.add("POST /api/v1/relay/set");
|
||||||
|
endpoints.add("GET /api/v1/config/wifi");
|
||||||
|
endpoints.add("POST /api/v1/config/wifi");
|
||||||
|
endpoints.add("POST /api/v1/wifi/connect");
|
||||||
|
endpoints.add("POST /api/v1/wifi/clear");
|
||||||
|
endpoints.add("POST /api/v1/temps/scan");
|
||||||
|
endpoints.add("POST /api/v1/temps/assign");
|
||||||
|
endpoints.add("POST /api/v1/temps/clear");
|
||||||
|
endpoints.add("POST /api/v1/bms/setup/enter");
|
||||||
|
endpoints.add("POST /api/v1/bms/setup/exit");
|
||||||
|
endpoints.add("POST /api/v1/bms/scan");
|
||||||
|
endpoints.add("POST /api/v1/bms/select");
|
||||||
|
|
||||||
|
JsonObject limits = doc.createNestedObject("limits");
|
||||||
|
limits["relay_count"] = MAX_RELAYS;
|
||||||
|
limits["temperature_sensor_count"] = MAX_TEMP_SENSORS;
|
||||||
|
limits["runtime_temperature_status_count"] = appConfig.tempSensorCount > 4 ? 4 : appConfig.tempSensorCount;
|
||||||
|
limits["wifi_network_count"] = MAX_WIFI_NETWORKS;
|
||||||
|
|
||||||
|
JsonObject features = doc.createNestedObject("features");
|
||||||
|
features["relay_control"] = true;
|
||||||
|
features["temperature_scan"] = true;
|
||||||
|
features["wifi_config"] = true;
|
||||||
|
features["config_backup_restore"] = true;
|
||||||
|
features["bms_setup"] = true;
|
||||||
|
features["uart_json"] = true;
|
||||||
|
features["root_compatibility_aliases"] = true;
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
server.send(200, "application/json", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleExportConfig() {
|
||||||
|
DynamicJsonDocument doc(6144);
|
||||||
|
|
||||||
|
doc["type"] = "config_export_response";
|
||||||
|
doc["ok"] = true;
|
||||||
|
doc["api_version"] = "v1";
|
||||||
|
doc["firmware_name"] = FIRMWARE_NAME;
|
||||||
|
doc["firmware_version"] = FIRMWARE_VERSION;
|
||||||
|
|
||||||
|
JsonObject config = doc.createNestedObject("config");
|
||||||
|
buildConfigDocument(config);
|
||||||
|
|
||||||
|
JsonObject wifi = doc.createNestedObject("wifi");
|
||||||
|
buildWifiConfigDocument(wifi, true);
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
server.send(200, "application/json", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleImportConfig() {
|
||||||
|
DynamicJsonDocument request(6144);
|
||||||
|
DeserializationError error = deserializeJson(request, server.arg("plain"));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject config = request["config"].as<JsonObject>();
|
||||||
|
JsonObject source = config.isNull() ? request.as<JsonObject>() : config;
|
||||||
|
|
||||||
|
if (!request["wifi"].isNull()) {
|
||||||
|
source["wifi"] = request["wifi"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* errorCode = nullptr;
|
||||||
|
if (!validateConfigImport(source, errorCode)) {
|
||||||
|
String response = "{\"ok\":false,\"error\":\"";
|
||||||
|
response += errorCode ? errorCode : "invalid_config";
|
||||||
|
response += "\"}";
|
||||||
|
server.send(400, "application/json", response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyImportedConfig(source);
|
||||||
|
sendConfigJson();
|
||||||
|
}
|
||||||
|
|
||||||
void handleUpdateDeviceConfig() {
|
void handleUpdateDeviceConfig() {
|
||||||
DynamicJsonDocument doc(512);
|
DynamicJsonDocument doc(512);
|
||||||
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
||||||
@@ -1995,27 +2305,7 @@ void sendWifiConfigJson() {
|
|||||||
doc["ok"] = true;
|
doc["ok"] = true;
|
||||||
|
|
||||||
JsonObject wifi = doc.createNestedObject("wifi");
|
JsonObject wifi = doc.createNestedObject("wifi");
|
||||||
wifi["ap_enabled"] = true;
|
buildWifiConfigDocument(wifi, false);
|
||||||
wifi["sta_enabled"] = wifiNetworkCount > 0;
|
|
||||||
wifi["network_count"] = wifiNetworkCount;
|
|
||||||
wifi["active_ssid"] = activeStaSsid;
|
|
||||||
wifi["sta_connected"] = WiFi.status() == WL_CONNECTED;
|
|
||||||
wifi["ap_ip"] = WiFi.softAPIP().toString();
|
|
||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
wifi["sta_ip"] = WiFi.localIP().toString();
|
|
||||||
} else {
|
|
||||||
wifi["sta_ip"] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonArray networks = wifi.createNestedArray("networks");
|
|
||||||
for (int i = 0; i < wifiNetworkCount; i++) {
|
|
||||||
JsonObject network = networks.createNestedObject();
|
|
||||||
network["index"] = i + 1;
|
|
||||||
network["ssid"] = staSsids[i];
|
|
||||||
network["priority"] = staPriorities[i];
|
|
||||||
network["password_set"] = staPasswords[i].length() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
String output;
|
String output;
|
||||||
serializeJson(doc, output);
|
serializeJson(doc, output);
|
||||||
@@ -2038,44 +2328,7 @@ void handleUpdateWifiConfig() {
|
|||||||
JsonArray networks = doc["networks"].as<JsonArray>();
|
JsonArray networks = doc["networks"].as<JsonArray>();
|
||||||
|
|
||||||
if (!networks.isNull()) {
|
if (!networks.isNull()) {
|
||||||
wifiNetworkCount = 0;
|
applyWifiNetworks(networks);
|
||||||
|
|
||||||
for (JsonObject network : networks) {
|
|
||||||
if (wifiNetworkCount >= MAX_WIFI_NETWORKS) break;
|
|
||||||
|
|
||||||
String ssid = network["ssid"] | "";
|
|
||||||
String password = network["password"] | "";
|
|
||||||
|
|
||||||
ssid.trim();
|
|
||||||
password.trim();
|
|
||||||
|
|
||||||
if (ssid.length() == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length() == 0) {
|
|
||||||
for (int oldIndex = 0; oldIndex < MAX_WIFI_NETWORKS; oldIndex++) {
|
|
||||||
if (staSsids[oldIndex] == ssid && staPasswords[oldIndex].length() > 0) {
|
|
||||||
password = staPasswords[oldIndex];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int priority = network["priority"] | (wifiNetworkCount + 1);
|
|
||||||
if (priority <= 0) priority = wifiNetworkCount + 1;
|
|
||||||
|
|
||||||
staSsids[wifiNetworkCount] = ssid;
|
|
||||||
staPasswords[wifiNetworkCount] = password;
|
|
||||||
staPriorities[wifiNetworkCount] = priority;
|
|
||||||
wifiNetworkCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = wifiNetworkCount; i < MAX_WIFI_NETWORKS; i++) {
|
|
||||||
staSsids[i] = "";
|
|
||||||
staPasswords[i] = "";
|
|
||||||
staPriorities[i] = i + 1;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (doc["ssid"].is<String>()) {
|
if (doc["ssid"].is<String>()) {
|
||||||
staSsids[0] = doc["ssid"].as<String>();
|
staSsids[0] = doc["ssid"].as<String>();
|
||||||
@@ -2435,9 +2688,13 @@ void setup() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on(API_V1("/status"), handleStatus);
|
server.on(API_V1("/status"), handleStatus);
|
||||||
|
server.on(API_V1("/health"), HTTP_GET, handleHealth);
|
||||||
|
server.on(API_V1("/capabilities"), HTTP_GET, handleCapabilities);
|
||||||
server.on(API_V1("/relay/set"), HTTP_POST, handleSetRelayPost);
|
server.on(API_V1("/relay/set"), HTTP_POST, handleSetRelayPost);
|
||||||
|
|
||||||
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
||||||
|
server.on(API_V1("/config/export"), HTTP_GET, handleExportConfig);
|
||||||
|
server.on(API_V1("/config/import"), HTTP_POST, handleImportConfig);
|
||||||
server.on(API_V1("/config/wifi"), HTTP_GET, handleGetWifiConfig);
|
server.on(API_V1("/config/wifi"), HTTP_GET, handleGetWifiConfig);
|
||||||
server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig);
|
server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig);
|
||||||
server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect);
|
server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect);
|
||||||
|
|||||||
+3
-1
@@ -20,12 +20,14 @@
|
|||||||
"address": "aa:bb:cc:dd:ee:ff",
|
"address": "aa:bb:cc:dd:ee:ff",
|
||||||
"address_type": "public"
|
"address_type": "public"
|
||||||
},
|
},
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
"temperature_sensors": [
|
"temperature_sensors": [
|
||||||
{
|
{
|
||||||
"id": "temp_1",
|
"id": "temp_1",
|
||||||
"name": "Cabin",
|
"name": "Cabin",
|
||||||
"address": "28ff001122334455",
|
"address": "28ff001122334455",
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"weather": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
"address": "aa:bb:cc:dd:ee:ff",
|
"address": "aa:bb:cc:dd:ee:ff",
|
||||||
"address_type": "public"
|
"address_type": "public"
|
||||||
},
|
},
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
"temperature_sensors": [
|
"temperature_sensors": [
|
||||||
{
|
{
|
||||||
"id": "temp_1",
|
"id": "temp_1",
|
||||||
|
|||||||
+3
-1
@@ -7,12 +7,14 @@
|
|||||||
"address": "aa:bb:cc:dd:ee:ff",
|
"address": "aa:bb:cc:dd:ee:ff",
|
||||||
"address_type": "public"
|
"address_type": "public"
|
||||||
},
|
},
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
"temperature_sensors": [
|
"temperature_sensors": [
|
||||||
{
|
{
|
||||||
"id": "temp_1",
|
"id": "temp_1",
|
||||||
"name": "Cabin",
|
"name": "Cabin",
|
||||||
"address": "28ff001122334455",
|
"address": "28ff001122334455",
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"weather": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -7,12 +7,14 @@
|
|||||||
"address": "aa:bb:cc:dd:ee:ff",
|
"address": "aa:bb:cc:dd:ee:ff",
|
||||||
"address_type": "public"
|
"address_type": "public"
|
||||||
},
|
},
|
||||||
|
"temperature_sensor_count": 4,
|
||||||
"temperature_sensors": [
|
"temperature_sensors": [
|
||||||
{
|
{
|
||||||
"id": "temp_1",
|
"id": "temp_1",
|
||||||
"name": "Cabin",
|
"name": "Cabin",
|
||||||
"address": "",
|
"address": "",
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"weather": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ def test_firmware_registers_current_http_contract_routes():
|
|||||||
|
|
||||||
expected_routes = {
|
expected_routes = {
|
||||||
("/api/v1/status", "ANY"),
|
("/api/v1/status", "ANY"),
|
||||||
|
("/api/v1/health", "HTTP_GET"),
|
||||||
|
("/api/v1/capabilities", "HTTP_GET"),
|
||||||
("/api/v1/config", "HTTP_GET"),
|
("/api/v1/config", "HTTP_GET"),
|
||||||
|
("/api/v1/config/export", "HTTP_GET"),
|
||||||
|
("/api/v1/config/import", "HTTP_POST"),
|
||||||
("/api/v1/relay/set", "HTTP_POST"),
|
("/api/v1/relay/set", "HTTP_POST"),
|
||||||
("/api/v1/config/wifi", "HTTP_GET"),
|
("/api/v1/config/wifi", "HTTP_GET"),
|
||||||
("/api/v1/config/wifi", "HTTP_POST"),
|
("/api/v1/config/wifi", "HTTP_POST"),
|
||||||
@@ -155,10 +159,42 @@ def test_status_fixture_matches_dashboard_contract_shape():
|
|||||||
def test_config_fixture_matches_current_config_response_shape():
|
def test_config_fixture_matches_current_config_response_shape():
|
||||||
payload = load_fixture("config_response.json")
|
payload = load_fixture("config_response.json")
|
||||||
|
|
||||||
assert_keys(payload, ["device_name", "relays", "bms", "temperature_sensors"])
|
assert_keys(payload, [
|
||||||
|
"device_name",
|
||||||
|
"relays",
|
||||||
|
"bms",
|
||||||
|
"temperature_sensor_count",
|
||||||
|
"temperature_sensors",
|
||||||
|
])
|
||||||
assert_keys(payload["relays"][0], ["id", "name", "pin", "enabled"])
|
assert_keys(payload["relays"][0], ["id", "name", "pin", "enabled"])
|
||||||
assert_keys(payload["bms"], ["enabled", "name", "address", "address_type"])
|
assert_keys(payload["bms"], ["enabled", "name", "address", "address_type"])
|
||||||
assert_keys(payload["temperature_sensors"][0], ["id", "name", "address", "enabled"])
|
assert_keys(payload["temperature_sensors"][0], [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"address",
|
||||||
|
"enabled",
|
||||||
|
"weather",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_and_capabilities_contracts_are_registered():
|
||||||
|
source = firmware_source()
|
||||||
|
|
||||||
|
assert '"health_response"' in source
|
||||||
|
assert '"capabilities_response"' in source
|
||||||
|
assert '"GET /api/v1/health"' in source
|
||||||
|
assert '"GET /api/v1/capabilities"' in source
|
||||||
|
assert '"config_backup_restore"' in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_backup_restore_contract_is_registered():
|
||||||
|
source = firmware_source()
|
||||||
|
|
||||||
|
assert 'API_V1("/config/export")' in source
|
||||||
|
assert 'API_V1("/config/import")' in source
|
||||||
|
assert '"config_export_response"' in source
|
||||||
|
assert '"password"' in source
|
||||||
|
assert '"invalid_config"' in source
|
||||||
|
|
||||||
|
|
||||||
def test_relay_set_contract_fixtures_and_firmware_errors():
|
def test_relay_set_contract_fixtures_and_firmware_errors():
|
||||||
@@ -236,6 +272,7 @@ def test_http_error_fixture_and_known_error_codes_are_stable():
|
|||||||
|
|
||||||
for error_code in [
|
for error_code in [
|
||||||
"invalid_json",
|
"invalid_json",
|
||||||
|
"invalid_config",
|
||||||
"unknown_relay",
|
"unknown_relay",
|
||||||
"unknown_temp_sensor",
|
"unknown_temp_sensor",
|
||||||
"invalid_temp_selection",
|
"invalid_temp_selection",
|
||||||
|
|||||||
Reference in New Issue
Block a user