chore: snapshot before shared RS485 bus integration
This commit is contained in:
2
components/rest_api/CMakeLists.txt
Executable file → Normal file
2
components/rest_api/CMakeLists.txt
Executable file → Normal file
@@ -5,7 +5,7 @@ set(srcs
|
||||
"src/auth_api.c"
|
||||
"src/network_api.c"
|
||||
"src/meters_settings_api.c"
|
||||
"src/loadbalancing_settings_api.c"
|
||||
"src/power_settings_api.c"
|
||||
"src/evse_link_config_api.c"
|
||||
"src/dashboard_api.c"
|
||||
"src/scheduler_settings_api.c"
|
||||
|
||||
0
components/rest_api/include/auth_api.h
Executable file → Normal file
0
components/rest_api/include/auth_api.h
Executable file → Normal file
0
components/rest_api/include/evse_settings_api.h
Executable file → Normal file
0
components/rest_api/include/evse_settings_api.h
Executable file → Normal file
0
components/rest_api/include/network_api.h
Executable file → Normal file
0
components/rest_api/include/network_api.h
Executable file → Normal file
0
components/rest_api/include/ocpp_api.h
Executable file → Normal file
0
components/rest_api/include/ocpp_api.h
Executable file → Normal file
23
components/rest_api/include/power_settings_api.h
Normal file
23
components/rest_api/include/power_settings_api.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef POWER_SETTINGS_API_H_
|
||||
#define POWER_SETTINGS_API_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
/**
|
||||
* @brief Register power configuration REST handlers.
|
||||
*
|
||||
* Endpoint:
|
||||
* GET /api/v1/config/power
|
||||
* POST /api/v1/config/power
|
||||
*/
|
||||
void register_power_settings_handlers(httpd_handle_t server, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* POWER_SETTINGS_API_H_ */
|
||||
0
components/rest_api/include/rest_main.h
Executable file → Normal file
0
components/rest_api/include/rest_main.h
Executable file → Normal file
0
components/rest_api/include/scheduler_settings_api.h
Executable file → Normal file
0
components/rest_api/include/scheduler_settings_api.h
Executable file → Normal file
0
components/rest_api/src/auth_api.c
Executable file → Normal file
0
components/rest_api/src/auth_api.c
Executable file → Normal file
73
components/rest_api/src/evse_settings_api.c
Executable file → Normal file
73
components/rest_api/src/evse_settings_api.c
Executable file → Normal file
@@ -8,61 +8,93 @@
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
static const char *TAG = "evse_settings_api";
|
||||
|
||||
static esp_err_t config_settings_get_handler(httpd_req_t *req) {
|
||||
static esp_err_t config_settings_get_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
|
||||
cJSON *config = cJSON_CreateObject();
|
||||
if (!config)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* currentLimit deixou de pertencer a /config/settings.
|
||||
* A corrente manual/static é configurada em /api/v1/config/power:
|
||||
*
|
||||
* {
|
||||
* "mode": "manual",
|
||||
* "manual": { "currentA": 16 }
|
||||
* }
|
||||
*
|
||||
* maxCurrentLimit fica aqui como informação read-only para a UI.
|
||||
*/
|
||||
cJSON_AddNumberToObject(config, "maxCurrentLimit", evse_get_max_charging_current());
|
||||
cJSON_AddNumberToObject(config, "currentLimit", evse_get_charging_current());
|
||||
cJSON_AddNumberToObject(config, "powerLimit", 0);
|
||||
cJSON_AddNumberToObject(config, "energyLimit", evse_get_consumption_limit());
|
||||
cJSON_AddNumberToObject(config, "chargingTimeLimit", evse_get_charging_time_limit());
|
||||
cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold());
|
||||
|
||||
cJSON *security = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(security, "earthFault", false);
|
||||
cJSON_AddBoolToObject(security, "rcm", false);
|
||||
cJSON_AddItemToObject(config, "security", security);
|
||||
if (security)
|
||||
{
|
||||
cJSON_AddBoolToObject(security, "earthFault", false);
|
||||
cJSON_AddBoolToObject(security, "rcm", false);
|
||||
cJSON_AddItemToObject(config, "security", security);
|
||||
}
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(config);
|
||||
httpd_resp_sendstr(req, json_str ? json_str : "{}");
|
||||
if (json_str) free(json_str);
|
||||
if (json_str)
|
||||
free(json_str);
|
||||
cJSON_Delete(config);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t config_settings_post_handler(httpd_req_t *req) {
|
||||
static esp_err_t config_settings_post_handler(httpd_req_t *req)
|
||||
{
|
||||
char buf[512];
|
||||
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (len <= 0) {
|
||||
if (len <= 0)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
buf[len] = '\0';
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json) {
|
||||
if (!json)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *current = cJSON_GetObjectItem(json, "currentLimit");
|
||||
if (cJSON_IsNumber(current)) {
|
||||
int value = current->valueint;
|
||||
int hw_max = evse_get_max_charging_current();
|
||||
if (value < 0) value = 0;
|
||||
if (value > hw_max) value = hw_max;
|
||||
evse_set_charging_current(value);
|
||||
/*
|
||||
* currentLimit é intencionalmente ignorado neste endpoint.
|
||||
* A UI nova não o envia. Se algum cliente antigo o enviar,
|
||||
* não altera a corrente para evitar duas fontes de verdade.
|
||||
*/
|
||||
if (cJSON_GetObjectItem(json, "currentLimit"))
|
||||
{
|
||||
ESP_LOGW(TAG, "Ignoring legacy currentLimit in /config/settings; use /config/power instead");
|
||||
}
|
||||
|
||||
cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit");
|
||||
if (cJSON_IsNumber(temp)) {
|
||||
if (cJSON_IsNumber(temp))
|
||||
{
|
||||
evse_set_temp_threshold(temp->valueint);
|
||||
}
|
||||
|
||||
// Campos aceites por compatibilidade de contrato v1.
|
||||
// Nesta sprint são ignorados caso o firmware ainda não os suporte internamente.
|
||||
/*
|
||||
* Campos aceites por compatibilidade de contrato v1.
|
||||
* Continuam ignorados se o firmware ainda não os suportar internamente.
|
||||
*/
|
||||
(void)cJSON_GetObjectItem(json, "powerLimit");
|
||||
(void)cJSON_GetObjectItem(json, "energyLimit");
|
||||
(void)cJSON_GetObjectItem(json, "chargingTimeLimit");
|
||||
@@ -73,7 +105,8 @@ static esp_err_t config_settings_post_handler(httpd_req_t *req) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void register_evse_settings_handlers(httpd_handle_t server, void *ctx) {
|
||||
void register_evse_settings_handlers(httpd_handle_t server, void *ctx)
|
||||
{
|
||||
httpd_uri_t get_uri = {
|
||||
.uri = "/api/v1/config/settings",
|
||||
.method = HTTP_GET,
|
||||
|
||||
@@ -8,15 +8,47 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static const char *TAG = "loadbalancing_settings_api";
|
||||
|
||||
// limites simples
|
||||
/*
|
||||
* REST API contract
|
||||
*
|
||||
* GET /api/v1/config/loadbalancing
|
||||
*
|
||||
* {
|
||||
* "mode": "off" | "manual" | "grid" | "solar",
|
||||
* "gridLimit": {
|
||||
* "enabled": true,
|
||||
* "maxImportA": 30
|
||||
* },
|
||||
* "solar": {
|
||||
* "maxImportW": 0
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Notes:
|
||||
* - "mode" is the source of truth.
|
||||
* - There is no top-level "enabled"; it is derived from mode != "off".
|
||||
* - There is no "solar.enabled"; it is derived from mode == "solar".
|
||||
* - There is no "solar.useGridLimit"; use gridLimit.enabled instead.
|
||||
* - In mode "solar", gridLimit.enabled=true means solar with grid import cap.
|
||||
* - In mode "off" or "manual", gridLimit/solar values are kept as saved config.
|
||||
*
|
||||
* Compatibility:
|
||||
* - POST still accepts legacy "enabled".
|
||||
* - POST still accepts legacy "pv" as alias for "solar".
|
||||
*/
|
||||
|
||||
#define MIN_GRID_A 6
|
||||
#define MAX_GRID_A 100
|
||||
|
||||
#define MIN_PV_W 0
|
||||
#define MAX_PV_W 100000 // ajusta se quiseres
|
||||
#define MAX_PV_W 100000
|
||||
|
||||
#define REQ_BODY_MAX 768
|
||||
|
||||
static esp_err_t send_json(httpd_req_t *req, cJSON *root)
|
||||
{
|
||||
@@ -25,7 +57,9 @@ static esp_err_t send_json(httpd_req_t *req, cJSON *root)
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
if (!json_str)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON encode failed");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"JSON encode failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -34,38 +68,11 @@ static esp_err_t send_json(httpd_req_t *req, cJSON *root)
|
||||
return err;
|
||||
}
|
||||
|
||||
// GET -> payload novo
|
||||
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled());
|
||||
|
||||
cJSON *grid = cJSON_CreateObject();
|
||||
cJSON_AddItemToObject(root, "gridLimit", grid);
|
||||
cJSON_AddBoolToObject(grid, "enabled", loadbalancer_grid_is_enabled());
|
||||
cJSON_AddNumberToObject(grid, "maxImportA", loadbalancer_grid_get_max_import_a());
|
||||
|
||||
cJSON *pv = cJSON_CreateObject();
|
||||
cJSON_AddItemToObject(root, "pv", pv);
|
||||
cJSON_AddBoolToObject(pv, "enabled", loadbalancer_pv_is_enabled());
|
||||
cJSON_AddNumberToObject(pv, "maxImportW", loadbalancer_pv_get_max_import_w());
|
||||
|
||||
esp_err_t err = send_json(req, root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
// lê body de forma robusta (httpd_req_recv pode devolver parcial)
|
||||
static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
|
||||
{
|
||||
if (req->content_len <= 0)
|
||||
return ESP_FAIL;
|
||||
|
||||
if ((size_t)req->content_len >= buf_sz)
|
||||
return ESP_ERR_NO_MEM;
|
||||
|
||||
@@ -77,6 +84,7 @@ static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
|
||||
int r = httpd_req_recv(req, buf + off, remaining);
|
||||
if (r <= 0)
|
||||
return ESP_FAIL;
|
||||
|
||||
off += r;
|
||||
remaining -= r;
|
||||
}
|
||||
@@ -85,31 +93,223 @@ static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// POST -> updates parciais aceites
|
||||
static bool mode_is_enabled(loadbalancer_limit_mode_t mode)
|
||||
{
|
||||
return mode != LOADBALANCER_LIMIT_MODE_OFF;
|
||||
}
|
||||
|
||||
static const char *rest_limit_mode_to_str(loadbalancer_limit_mode_t mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case LOADBALANCER_LIMIT_MODE_OFF:
|
||||
return "off";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_MANUAL:
|
||||
return "manual";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID:
|
||||
return "grid";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_SOLAR:
|
||||
return "solar";
|
||||
|
||||
default:
|
||||
return "off";
|
||||
}
|
||||
}
|
||||
|
||||
static bool rest_limit_mode_from_str(const char *str,
|
||||
loadbalancer_limit_mode_t *out)
|
||||
{
|
||||
if (!str || !out)
|
||||
return false;
|
||||
|
||||
if (strcmp(str, "off") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_OFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "manual") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_MANUAL;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "grid") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_GRID;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "solar") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_SOLAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Aliases legacy.
|
||||
*/
|
||||
if (strcmp(str, "pv") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_SOLAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "loadbalancer") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_GRID;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fallback para parser do módulo, caso tenha aliases adicionais.
|
||||
*/
|
||||
return loadbalancer_limit_mode_from_str(str, out);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compatibilidade com payload antigo:
|
||||
*
|
||||
* {
|
||||
* "enabled": true,
|
||||
* "gridLimit": { "enabled": true },
|
||||
* "pv": { "enabled": false }
|
||||
* }
|
||||
*/
|
||||
static loadbalancer_limit_mode_t derive_mode_from_legacy(bool enabled,
|
||||
bool grid_enabled,
|
||||
bool solar_enabled)
|
||||
{
|
||||
if (!enabled)
|
||||
return LOADBALANCER_LIMIT_MODE_OFF;
|
||||
|
||||
/*
|
||||
* SOLAR tem prioridade.
|
||||
* Em modo SOLAR, gridLimit.enabled pode continuar true
|
||||
* para limitar a importação da rede.
|
||||
*/
|
||||
if (solar_enabled)
|
||||
return LOADBALANCER_LIMIT_MODE_SOLAR;
|
||||
|
||||
if (grid_enabled)
|
||||
return LOADBALANCER_LIMIT_MODE_GRID;
|
||||
|
||||
/*
|
||||
* Compatibilidade com UI antiga:
|
||||
* se vier só {"enabled": true}, assume GRID.
|
||||
*/
|
||||
return LOADBALANCER_LIMIT_MODE_GRID;
|
||||
}
|
||||
|
||||
// GET -> payload novo e limpo, baseado em mode
|
||||
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
loadbalancer_limit_mode_t mode = loadbalancer_get_limit_mode();
|
||||
|
||||
/*
|
||||
* Campo principal.
|
||||
*/
|
||||
cJSON_AddStringToObject(root, "mode", rest_limit_mode_to_str(mode));
|
||||
|
||||
/*
|
||||
* Configuração do limite de importação da rede.
|
||||
*
|
||||
* Em mode=grid:
|
||||
* gridLimit.enabled deve estar true.
|
||||
*
|
||||
* Em mode=solar:
|
||||
* gridLimit.enabled=true significa solar com limite máximo
|
||||
* de importação da rede.
|
||||
*/
|
||||
cJSON *grid = cJSON_CreateObject();
|
||||
if (!grid)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "gridLimit", grid);
|
||||
cJSON_AddBoolToObject(grid,
|
||||
"enabled",
|
||||
loadbalancer_grid_is_enabled());
|
||||
cJSON_AddNumberToObject(grid,
|
||||
"maxImportA",
|
||||
loadbalancer_grid_get_max_import_a());
|
||||
|
||||
/*
|
||||
* Novo bloco REST: solar.
|
||||
* Internamente ainda usa funções pv_*.
|
||||
*/
|
||||
cJSON *solar = cJSON_CreateObject();
|
||||
if (!solar)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "solar", solar);
|
||||
cJSON_AddNumberToObject(solar,
|
||||
"maxImportW",
|
||||
loadbalancer_pv_get_max_import_w());
|
||||
|
||||
esp_err_t err = send_json(req, root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
// POST -> updates parciais aceites, mas "mode" é o campo principal
|
||||
static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req)
|
||||
{
|
||||
if (req->content_len <= 0)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Empty body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (req->content_len >= 512)
|
||||
if (req->content_len >= REQ_BODY_MAX)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_413_CONTENT_TOO_LARGE,
|
||||
"Body too large");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char buf[512];
|
||||
char buf[REQ_BODY_MAX];
|
||||
|
||||
esp_err_t rb = read_body(req, buf, sizeof(buf));
|
||||
if (rb == ESP_ERR_NO_MEM)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_413_CONTENT_TOO_LARGE,
|
||||
"Body too large");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (rb != ESP_OK)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Failed to read body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -118,81 +318,419 @@ static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req)
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// enabled (top-level)
|
||||
cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled");
|
||||
if (enabled_item && cJSON_IsBool(enabled_item))
|
||||
if (!cJSON_IsObject(json))
|
||||
{
|
||||
loadbalancer_set_enabled(cJSON_IsTrue(enabled_item));
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"JSON root must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// gridLimit
|
||||
cJSON *grid = cJSON_GetObjectItem(json, "gridLimit");
|
||||
if (grid && cJSON_IsObject(grid))
|
||||
/*
|
||||
* Base = estado atual.
|
||||
* Isto permite updates parciais sem limpar configuração.
|
||||
*/
|
||||
loadbalancer_limit_mode_t next_mode = loadbalancer_get_limit_mode();
|
||||
|
||||
bool next_grid_enabled = loadbalancer_grid_is_enabled();
|
||||
uint8_t next_grid_max_a = loadbalancer_grid_get_max_import_a();
|
||||
|
||||
/*
|
||||
* Internamente o módulo ainda usa pv_*.
|
||||
* Na REST nova chamamos "solar".
|
||||
*/
|
||||
bool next_solar_enabled = loadbalancer_pv_is_enabled();
|
||||
int32_t next_solar_max_w = loadbalancer_pv_get_max_import_w();
|
||||
|
||||
bool mode_provided = false;
|
||||
|
||||
/*
|
||||
* Campos legacy só são usados para derivar mode se "mode"
|
||||
* não vier no payload.
|
||||
*/
|
||||
bool legacy_operational_hint = false;
|
||||
bool legacy_enabled = mode_is_enabled(next_mode);
|
||||
|
||||
/*
|
||||
* Campo novo/principal:
|
||||
*
|
||||
* {
|
||||
* "mode": "off" | "manual" | "grid" | "solar"
|
||||
* }
|
||||
*/
|
||||
cJSON *mode_item = cJSON_GetObjectItem(json, "mode");
|
||||
if (mode_item)
|
||||
{
|
||||
cJSON *g_en = cJSON_GetObjectItem(grid, "enabled");
|
||||
if (g_en && cJSON_IsBool(g_en))
|
||||
if (!cJSON_IsString(mode_item) || !mode_item->valuestring)
|
||||
{
|
||||
loadbalancer_grid_set_enabled(cJSON_IsTrue(g_en));
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"mode must be string");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!rest_limit_mode_from_str(mode_item->valuestring, &next_mode))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
mode_provided = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Campo legacy:
|
||||
*
|
||||
* {
|
||||
* "enabled": true
|
||||
* }
|
||||
*
|
||||
* Não é emitido pelo GET novo.
|
||||
* Só é aceite para compatibilidade.
|
||||
*/
|
||||
cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled");
|
||||
if (enabled_item)
|
||||
{
|
||||
if (!cJSON_IsBool(enabled_item))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"enabled must be boolean");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
legacy_enabled = cJSON_IsTrue(enabled_item);
|
||||
legacy_operational_hint = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* gridLimit
|
||||
*/
|
||||
cJSON *grid = cJSON_GetObjectItem(json, "gridLimit");
|
||||
if (grid)
|
||||
{
|
||||
if (!cJSON_IsObject(grid))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"gridLimit must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *g_en = cJSON_GetObjectItem(grid, "enabled");
|
||||
if (g_en)
|
||||
{
|
||||
if (!cJSON_IsBool(g_en))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"gridLimit.enabled must be boolean");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
next_grid_enabled = cJSON_IsTrue(g_en);
|
||||
legacy_operational_hint = true;
|
||||
}
|
||||
|
||||
cJSON *g_maxA = cJSON_GetObjectItem(grid, "maxImportA");
|
||||
if (g_maxA && cJSON_IsNumber(g_maxA))
|
||||
if (g_maxA)
|
||||
{
|
||||
if (!cJSON_IsNumber(g_maxA))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"gridLimit.maxImportA must be number");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int maxA = (int)g_maxA->valuedouble;
|
||||
|
||||
if (maxA < MIN_GRID_A || maxA > MAX_GRID_A)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "gridLimit.maxImportA must be 6-100");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"gridLimit.maxImportA must be 6-100");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)maxA);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set gridLimit.maxImportA");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
next_grid_max_a = (uint8_t)maxA;
|
||||
}
|
||||
}
|
||||
|
||||
// pv
|
||||
cJSON *pv = cJSON_GetObjectItem(json, "pv");
|
||||
if (pv && cJSON_IsObject(pv))
|
||||
/*
|
||||
* Bloco novo: solar
|
||||
*
|
||||
* {
|
||||
* "solar": {
|
||||
* "maxImportW": 0
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Compatibilidade:
|
||||
* ainda aceita "pv" como alias antigo de "solar".
|
||||
*/
|
||||
cJSON *solar = cJSON_GetObjectItem(json, "solar");
|
||||
if (!solar)
|
||||
solar = cJSON_GetObjectItem(json, "pv");
|
||||
|
||||
if (solar)
|
||||
{
|
||||
cJSON *p_en = cJSON_GetObjectItem(pv, "enabled");
|
||||
if (p_en && cJSON_IsBool(p_en))
|
||||
if (!cJSON_IsObject(solar))
|
||||
{
|
||||
loadbalancer_pv_set_enabled(cJSON_IsTrue(p_en));
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *p_maxW = cJSON_GetObjectItem(pv, "maxImportW");
|
||||
if (p_maxW && cJSON_IsNumber(p_maxW))
|
||||
/*
|
||||
* Campo legacy:
|
||||
*
|
||||
* {
|
||||
* "solar": { "enabled": true }
|
||||
* }
|
||||
*
|
||||
* Não é emitido pelo GET novo.
|
||||
* No contrato novo, quem ativa solar é:
|
||||
*
|
||||
* {
|
||||
* "mode": "solar"
|
||||
* }
|
||||
*/
|
||||
cJSON *s_en = cJSON_GetObjectItem(solar, "enabled");
|
||||
if (s_en)
|
||||
{
|
||||
int32_t maxW = (int32_t)p_maxW->valuedouble;
|
||||
if (!cJSON_IsBool(s_en))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.enabled must be boolean");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
next_solar_enabled = cJSON_IsTrue(s_en);
|
||||
legacy_operational_hint = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Campo legacy removido do GET:
|
||||
*
|
||||
* {
|
||||
* "solar": { "useGridLimit": true }
|
||||
* }
|
||||
*
|
||||
* Continua aceite no POST e mapeia para gridLimit.enabled.
|
||||
*/
|
||||
cJSON *s_use_grid = cJSON_GetObjectItem(solar, "useGridLimit");
|
||||
if (s_use_grid)
|
||||
{
|
||||
if (!cJSON_IsBool(s_use_grid))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.useGridLimit must be boolean");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
next_grid_enabled = cJSON_IsTrue(s_use_grid);
|
||||
}
|
||||
|
||||
cJSON *s_maxW = cJSON_GetObjectItem(solar, "maxImportW");
|
||||
if (s_maxW)
|
||||
{
|
||||
if (!cJSON_IsNumber(s_maxW))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.maxImportW must be number");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int32_t maxW = (int32_t)s_maxW->valuedouble;
|
||||
|
||||
if (maxW < MIN_PV_W || maxW > MAX_PV_W)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "pv.maxImportW out of range");
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.maxImportW out of range");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t e = loadbalancer_pv_set_max_import_w(maxW);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set pv.maxImportW");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
next_solar_max_w = maxW;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Se o payload não trouxe "mode", aceitamos a semântica antiga
|
||||
* e derivamos o modo a partir de enabled/grid/pv.
|
||||
*
|
||||
* Se o payload só alterou maxImportA ou maxImportW,
|
||||
* mantemos o modo atual.
|
||||
*/
|
||||
if (!mode_provided && legacy_operational_hint)
|
||||
{
|
||||
next_mode = derive_mode_from_legacy(legacy_enabled,
|
||||
next_grid_enabled,
|
||||
next_solar_enabled);
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalização.
|
||||
*
|
||||
* "mode" é a fonte de verdade.
|
||||
* gridLimit e solar são configuração auxiliar.
|
||||
*/
|
||||
switch (next_mode)
|
||||
{
|
||||
case LOADBALANCER_LIMIT_MODE_OFF:
|
||||
/*
|
||||
* OFF não apaga gridLimit nem solar.
|
||||
* Apenas o modo operacional fica OFF.
|
||||
*/
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_MANUAL:
|
||||
/*
|
||||
* MANUAL não usa algoritmo dinâmico.
|
||||
* Não apagar configuração grid/solar.
|
||||
*/
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID:
|
||||
/*
|
||||
* GRID implica grid limiter ativo.
|
||||
* Não precisa de solar ativo.
|
||||
*/
|
||||
next_grid_enabled = true;
|
||||
next_solar_enabled = false;
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_SOLAR:
|
||||
/*
|
||||
* SOLAR implica solar ativo.
|
||||
* Não forçar grid=false.
|
||||
*
|
||||
* Em modo solar:
|
||||
* gridLimit.enabled=true
|
||||
* significa:
|
||||
* solar com limite máximo de importação da rede.
|
||||
*/
|
||||
next_solar_enabled = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t e;
|
||||
|
||||
/*
|
||||
* 1) Parâmetros.
|
||||
*/
|
||||
e = loadbalancer_grid_set_max_import_a(next_grid_max_a);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set gridLimit.maxImportA: %s",
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set gridLimit.maxImportA");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
e = loadbalancer_pv_set_max_import_w(next_solar_max_w);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set solar.maxImportW: %s",
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set solar.maxImportW");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 2) Configuração auxiliar.
|
||||
*
|
||||
* Estes setters guardam opções auxiliares.
|
||||
* O estado operacional principal continua a ser "mode".
|
||||
*/
|
||||
loadbalancer_grid_set_enabled(next_grid_enabled);
|
||||
loadbalancer_pv_set_enabled(next_solar_enabled);
|
||||
|
||||
/*
|
||||
* 3) Estado operacional principal.
|
||||
*
|
||||
* Isto resolve o problema após reset:
|
||||
* o REST passa a gravar o mesmo campo que o loadbalancer_load_config()
|
||||
* usa no boot.
|
||||
*/
|
||||
e = loadbalancer_set_limit_mode(next_mode);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set loadbalancer mode: %s",
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set loadbalancer mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 4) Garantir persistência antes de responder OK.
|
||||
*/
|
||||
e = loadbalancer_flush_config(2000);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to persist loadbalancer config: %s",
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to persist loadbalancer config");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"Loadbalancing saved: mode=%s gridEnabled=%d maxA=%u solarEnabled=%d solarMaxW=%ld",
|
||||
rest_limit_mode_to_str(next_mode),
|
||||
next_grid_enabled,
|
||||
(unsigned)next_grid_max_a,
|
||||
next_solar_enabled,
|
||||
(long)next_solar_max_w);
|
||||
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_sendstr(req, "OK");
|
||||
return ESP_OK;
|
||||
@@ -204,13 +742,17 @@ void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx)
|
||||
.uri = "/api/v1/config/loadbalancing",
|
||||
.method = HTTP_GET,
|
||||
.handler = loadbalancing_config_get_handler,
|
||||
.user_ctx = ctx};
|
||||
.user_ctx = ctx
|
||||
};
|
||||
|
||||
httpd_register_uri_handler(server, &get_uri);
|
||||
|
||||
httpd_uri_t post_uri = {
|
||||
.uri = "/api/v1/config/loadbalancing",
|
||||
.method = HTTP_POST,
|
||||
.handler = loadbalancing_config_post_handler,
|
||||
.user_ctx = ctx};
|
||||
.user_ctx = ctx
|
||||
};
|
||||
|
||||
httpd_register_uri_handler(server, &post_uri);
|
||||
}
|
||||
}
|
||||
0
components/rest_api/src/network_api.c
Executable file → Normal file
0
components/rest_api/src/network_api.c
Executable file → Normal file
0
components/rest_api/src/ocpp_api.c
Executable file → Normal file
0
components/rest_api/src/ocpp_api.c
Executable file → Normal file
692
components/rest_api/src/power_settings_api.c
Normal file
692
components/rest_api/src/power_settings_api.c
Normal file
@@ -0,0 +1,692 @@
|
||||
#include "power_settings_api.h"
|
||||
|
||||
#include "loadbalancer.h"
|
||||
#include "evse_config.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "power_settings_api";
|
||||
|
||||
/*
|
||||
* REST API contract
|
||||
*
|
||||
* Endpoint:
|
||||
* GET /api/v1/config/power
|
||||
* POST /api/v1/config/power
|
||||
*
|
||||
* JSON:
|
||||
*
|
||||
* {
|
||||
* "mode": "off" | "manual" | "grid" | "solar" | "grid_solar",
|
||||
* "manual": {
|
||||
* "currentA": 16
|
||||
* },
|
||||
* "grid": {
|
||||
* "maxImportA": 30
|
||||
* },
|
||||
* "solar": {
|
||||
* "maxImportW": 0
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Notes:
|
||||
* - "mode" is the source of truth.
|
||||
* - There is no top-level "enabled".
|
||||
* - There is no "solar.enabled".
|
||||
* - There is no "solar.gridCap" in the clean API.
|
||||
* - "manual.currentA" is the static/default charging current.
|
||||
* - "grid.maxImportA" is the maximum grid import current.
|
||||
* - "solar.maxImportW" is the maximum allowed import while in solar mode.
|
||||
*
|
||||
* Mode behavior:
|
||||
* - off:
|
||||
* no dynamic power policy is active.
|
||||
*
|
||||
* - manual:
|
||||
* no dynamic power policy is active; EVSE uses manual.currentA.
|
||||
*
|
||||
* - grid:
|
||||
* grid import limiter is active and uses grid.maxImportA.
|
||||
*
|
||||
* - solar:
|
||||
* solar/surplus optimizer is active and uses solar.maxImportW.
|
||||
* The grid limiter is not active.
|
||||
*
|
||||
* - grid_solar:
|
||||
* solar/surplus optimizer is active and also respects grid.maxImportA.
|
||||
*
|
||||
* Compatibility:
|
||||
* - POST still accepts legacy solar.gridCap. If mode=solar and gridCap=true,
|
||||
* it is converted to mode=grid_solar.
|
||||
*/
|
||||
|
||||
#define MIN_MANUAL_A 6
|
||||
|
||||
#define MIN_GRID_A 6
|
||||
#define MAX_GRID_A 100
|
||||
|
||||
#define MIN_SOLAR_W 0
|
||||
#define MAX_SOLAR_W 100000
|
||||
|
||||
#define REQ_BODY_MAX 768
|
||||
|
||||
static esp_err_t send_json(httpd_req_t *req, cJSON *root)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
if (!json_str)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"JSON encode failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = httpd_resp_sendstr(req, json_str);
|
||||
free(json_str);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
|
||||
{
|
||||
if (req->content_len <= 0)
|
||||
return ESP_FAIL;
|
||||
|
||||
if ((size_t)req->content_len >= buf_sz)
|
||||
return ESP_ERR_NO_MEM;
|
||||
|
||||
int remaining = req->content_len;
|
||||
int off = 0;
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int r = httpd_req_recv(req, buf + off, remaining);
|
||||
if (r <= 0)
|
||||
return ESP_FAIL;
|
||||
|
||||
off += r;
|
||||
remaining -= r;
|
||||
}
|
||||
|
||||
buf[off] = '\0';
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const char *power_mode_to_str(loadbalancer_limit_mode_t mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case LOADBALANCER_LIMIT_MODE_OFF:
|
||||
return "off";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_MANUAL:
|
||||
return "manual";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID:
|
||||
return "grid";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_SOLAR:
|
||||
return "solar";
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
|
||||
return "grid_solar";
|
||||
|
||||
default:
|
||||
return "off";
|
||||
}
|
||||
}
|
||||
|
||||
static bool power_mode_from_str(const char *str, loadbalancer_limit_mode_t *out)
|
||||
{
|
||||
if (!str || !out)
|
||||
return false;
|
||||
|
||||
if (strcmp(str, "off") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_OFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "manual") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_MANUAL;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "grid") == 0 ||
|
||||
strcmp(str, "loadbalancer") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_GRID;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "solar") == 0 ||
|
||||
strcmp(str, "pv") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_SOLAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(str, "grid_solar") == 0 ||
|
||||
strcmp(str, "solar_grid") == 0 ||
|
||||
strcmp(str, "grid_pv") == 0 ||
|
||||
strcmp(str, "pv_grid") == 0)
|
||||
{
|
||||
*out = LOADBALANCER_LIMIT_MODE_GRID_SOLAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static esp_err_t parse_object_if_present(cJSON *root,
|
||||
const char *name,
|
||||
cJSON **out)
|
||||
{
|
||||
cJSON *obj = cJSON_GetObjectItem(root, name);
|
||||
if (!obj)
|
||||
{
|
||||
*out = NULL;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!cJSON_IsObject(obj))
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
|
||||
*out = obj;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t parse_int_field_if_present(cJSON *obj,
|
||||
const char *name,
|
||||
int32_t min_value,
|
||||
int32_t max_value,
|
||||
int32_t *out,
|
||||
bool *provided)
|
||||
{
|
||||
if (provided)
|
||||
*provided = false;
|
||||
|
||||
if (!obj)
|
||||
return ESP_OK;
|
||||
|
||||
cJSON *item = cJSON_GetObjectItem(obj, name);
|
||||
if (!item)
|
||||
return ESP_OK;
|
||||
|
||||
if (!cJSON_IsNumber(item))
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
|
||||
int32_t value = (int32_t)item->valuedouble;
|
||||
|
||||
if (value < min_value || value > max_value)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
|
||||
*out = value;
|
||||
|
||||
if (provided)
|
||||
*provided = true;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t power_config_get_handler(httpd_req_t *req)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
loadbalancer_limit_mode_t mode = loadbalancer_get_limit_mode();
|
||||
|
||||
cJSON_AddStringToObject(root, "mode", power_mode_to_str(mode));
|
||||
|
||||
cJSON *manual = cJSON_CreateObject();
|
||||
if (!manual)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "manual", manual);
|
||||
cJSON_AddNumberToObject(manual,
|
||||
"currentA",
|
||||
(int)evse_get_charging_current());
|
||||
|
||||
cJSON *grid = cJSON_CreateObject();
|
||||
if (!grid)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "grid", grid);
|
||||
cJSON_AddNumberToObject(grid,
|
||||
"maxImportA",
|
||||
(int)loadbalancer_grid_get_max_import_a());
|
||||
|
||||
cJSON *solar = cJSON_CreateObject();
|
||||
if (!solar)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"No mem");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "solar", solar);
|
||||
cJSON_AddNumberToObject(solar,
|
||||
"maxImportW",
|
||||
(int)loadbalancer_pv_get_max_import_w());
|
||||
|
||||
esp_err_t err = send_json(req, root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t power_config_post_handler(httpd_req_t *req)
|
||||
{
|
||||
if (req->content_len <= 0)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Empty body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (req->content_len >= REQ_BODY_MAX)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_413_CONTENT_TOO_LARGE,
|
||||
"Body too large");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char buf[REQ_BODY_MAX];
|
||||
|
||||
esp_err_t rb = read_body(req, buf, sizeof(buf));
|
||||
if (rb == ESP_ERR_NO_MEM)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_413_CONTENT_TOO_LARGE,
|
||||
"Body too large");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (rb != ESP_OK)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Failed to read body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "POST: %s", buf);
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json)
|
||||
{
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!cJSON_IsObject(json))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"JSON root must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Base = current runtime/config state.
|
||||
* This allows partial updates without clearing other values.
|
||||
*/
|
||||
loadbalancer_limit_mode_t next_mode = loadbalancer_get_limit_mode();
|
||||
|
||||
uint16_t next_manual_current_a = evse_get_charging_current();
|
||||
uint8_t next_grid_max_a = loadbalancer_grid_get_max_import_a();
|
||||
int32_t next_solar_max_w = loadbalancer_pv_get_max_import_w();
|
||||
|
||||
/*
|
||||
* mode
|
||||
*/
|
||||
cJSON *mode_item = cJSON_GetObjectItem(json, "mode");
|
||||
if (mode_item)
|
||||
{
|
||||
if (!cJSON_IsString(mode_item) || !mode_item->valuestring)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"mode must be string");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!power_mode_from_str(mode_item->valuestring, &next_mode))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* manual
|
||||
*/
|
||||
cJSON *manual = NULL;
|
||||
esp_err_t e = parse_object_if_present(json, "manual", &manual);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"manual must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
int32_t current_a = (int32_t)next_manual_current_a;
|
||||
bool provided = false;
|
||||
|
||||
e = parse_int_field_if_present(manual,
|
||||
"currentA",
|
||||
MIN_MANUAL_A,
|
||||
evse_get_max_charging_current(),
|
||||
¤t_a,
|
||||
&provided);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"manual.currentA out of range");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (provided)
|
||||
next_manual_current_a = (uint16_t)current_a;
|
||||
}
|
||||
|
||||
/*
|
||||
* grid
|
||||
*/
|
||||
cJSON *grid = NULL;
|
||||
e = parse_object_if_present(json, "grid", &grid);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"grid must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (grid)
|
||||
{
|
||||
int32_t max_import_a = (int32_t)next_grid_max_a;
|
||||
bool provided = false;
|
||||
|
||||
e = parse_int_field_if_present(grid,
|
||||
"maxImportA",
|
||||
MIN_GRID_A,
|
||||
MAX_GRID_A,
|
||||
&max_import_a,
|
||||
&provided);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"grid.maxImportA must be 6-100");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (provided)
|
||||
next_grid_max_a = (uint8_t)max_import_a;
|
||||
}
|
||||
|
||||
/*
|
||||
* solar
|
||||
*/
|
||||
cJSON *solar = NULL;
|
||||
e = parse_object_if_present(json, "solar", &solar);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar must be object");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (solar)
|
||||
{
|
||||
int32_t max_import_w = next_solar_max_w;
|
||||
bool provided = false;
|
||||
|
||||
e = parse_int_field_if_present(solar,
|
||||
"maxImportW",
|
||||
MIN_SOLAR_W,
|
||||
MAX_SOLAR_W,
|
||||
&max_import_w,
|
||||
&provided);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.maxImportW out of range");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (provided)
|
||||
next_solar_max_w = max_import_w;
|
||||
|
||||
/*
|
||||
* Legacy compatibility only:
|
||||
*
|
||||
* Old clients may still send:
|
||||
* "mode": "solar",
|
||||
* "solar": { "gridCap": true }
|
||||
*
|
||||
* The clean API uses:
|
||||
* "mode": "grid_solar"
|
||||
*
|
||||
* If gridCap is present, convert the mode accordingly.
|
||||
*/
|
||||
cJSON *legacy_grid_cap = cJSON_GetObjectItem(solar, "gridCap");
|
||||
if (legacy_grid_cap)
|
||||
{
|
||||
if (!cJSON_IsBool(legacy_grid_cap))
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"solar.gridCap must be boolean");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (next_mode == LOADBALANCER_LIMIT_MODE_SOLAR &&
|
||||
cJSON_IsTrue(legacy_grid_cap))
|
||||
{
|
||||
next_mode = LOADBALANCER_LIMIT_MODE_GRID_SOLAR;
|
||||
}
|
||||
else if (next_mode == LOADBALANCER_LIMIT_MODE_GRID_SOLAR &&
|
||||
!cJSON_IsTrue(legacy_grid_cap))
|
||||
{
|
||||
next_mode = LOADBALANCER_LIMIT_MODE_SOLAR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalize auxiliary flags from the selected mode.
|
||||
*
|
||||
* mode is the source of truth.
|
||||
*/
|
||||
bool apply_grid_limiter = false;
|
||||
bool apply_solar_optimizer = false;
|
||||
|
||||
switch (next_mode)
|
||||
{
|
||||
case LOADBALANCER_LIMIT_MODE_OFF:
|
||||
apply_grid_limiter = false;
|
||||
apply_solar_optimizer = false;
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_MANUAL:
|
||||
apply_grid_limiter = false;
|
||||
apply_solar_optimizer = false;
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID:
|
||||
apply_grid_limiter = true;
|
||||
apply_solar_optimizer = false;
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_SOLAR:
|
||||
apply_grid_limiter = false;
|
||||
apply_solar_optimizer = true;
|
||||
break;
|
||||
|
||||
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
|
||||
apply_grid_limiter = true;
|
||||
apply_solar_optimizer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_400_BAD_REQUEST,
|
||||
"Invalid mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1) Static/manual current.
|
||||
*/
|
||||
e = evse_set_charging_current(next_manual_current_a);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set manual.currentA=%u: %s",
|
||||
(unsigned)next_manual_current_a,
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set manual.currentA");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 2) Loadbalancer parameters.
|
||||
*/
|
||||
e = loadbalancer_grid_set_max_import_a(next_grid_max_a);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set grid.maxImportA=%u: %s",
|
||||
(unsigned)next_grid_max_a,
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set grid.maxImportA");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
e = loadbalancer_pv_set_max_import_w(next_solar_max_w);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set solar.maxImportW=%ld: %s",
|
||||
(long)next_solar_max_w,
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set solar.maxImportW");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 3) Auxiliary flags derived from mode.
|
||||
*/
|
||||
loadbalancer_grid_set_enabled(apply_grid_limiter);
|
||||
loadbalancer_pv_set_enabled(apply_solar_optimizer);
|
||||
|
||||
/*
|
||||
* 4) Operational mode.
|
||||
*/
|
||||
e = loadbalancer_set_limit_mode(next_mode);
|
||||
if (e != ESP_OK)
|
||||
{
|
||||
cJSON_Delete(json);
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to set mode=%s: %s",
|
||||
power_mode_to_str(next_mode),
|
||||
esp_err_to_name(e));
|
||||
httpd_resp_send_err(req,
|
||||
HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to set power mode");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"Power config saved: mode=%s manual=%uA gridMax=%uA solarMax=%ldW",
|
||||
power_mode_to_str(next_mode),
|
||||
(unsigned)next_manual_current_a,
|
||||
(unsigned)next_grid_max_a,
|
||||
(long)next_solar_max_w);
|
||||
|
||||
cJSON_Delete(json);
|
||||
httpd_resp_sendstr(req, "OK");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void register_power_settings_handlers(httpd_handle_t server, void *ctx)
|
||||
{
|
||||
httpd_uri_t get_uri = {
|
||||
.uri = "/api/v1/config/power",
|
||||
.method = HTTP_GET,
|
||||
.handler = power_config_get_handler,
|
||||
.user_ctx = ctx};
|
||||
|
||||
httpd_register_uri_handler(server, &get_uri);
|
||||
|
||||
httpd_uri_t post_uri = {
|
||||
.uri = "/api/v1/config/power",
|
||||
.method = HTTP_POST,
|
||||
.handler = power_config_post_handler,
|
||||
.user_ctx = ctx};
|
||||
|
||||
httpd_register_uri_handler(server, &post_uri);
|
||||
}
|
||||
4
components/rest_api/src/rest_main.c
Executable file → Normal file
4
components/rest_api/src/rest_main.c
Executable file → Normal file
@@ -1,7 +1,7 @@
|
||||
#include "rest_main.h"
|
||||
#include "evse_settings_api.h"
|
||||
#include "meters_settings_api.h"
|
||||
#include "loadbalancing_settings_api.h"
|
||||
#include "power_settings_api.h"
|
||||
#include "evse_link_config_api.h"
|
||||
#include "network_api.h"
|
||||
#include "ocpp_api.h"
|
||||
@@ -59,7 +59,7 @@ esp_err_t rest_server_init(const char *base_path)
|
||||
register_auth_handlers(server, ctx);
|
||||
register_dashboard_handlers(server, ctx);
|
||||
register_meters_settings_handlers(server, ctx);
|
||||
register_loadbalancing_settings_handlers(server, ctx);
|
||||
register_power_settings_handlers(server, ctx);
|
||||
register_link_config_handlers(server, ctx);
|
||||
register_meters_data_handlers(server, ctx);
|
||||
register_scheduler_settings_handlers(server, ctx);
|
||||
|
||||
0
components/rest_api/src/scheduler_settings_api.c
Executable file → Normal file
0
components/rest_api/src/scheduler_settings_api.c
Executable file → Normal file
51
components/rest_api/webfolder/assets/index-BSlegejp.js
Normal file
51
components/rest_api/webfolder/assets/index-BSlegejp.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,8 +13,8 @@
|
||||
}
|
||||
</style>
|
||||
<title>ChargeFlow</title>
|
||||
<script type="module" crossorigin src="/assets/index-CBPnRb7F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-5y1tcufv.css">
|
||||
<script type="module" crossorigin src="/assets/index-BSlegejp.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-vUXH8POW.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user