chore: snapshot before shared RS485 bus integration

This commit is contained in:
2026-07-22 15:36:04 +01:00
parent ef02e5c5f5
commit 2a803fcef1
167 changed files with 5749 additions and 1128 deletions

0
components/loadbalancer/CMakeLists.txt Executable file → Normal file
View File

0
components/loadbalancer/include/grid_limiter.h Executable file → Normal file
View File

0
components/loadbalancer/include/input_filter.h Executable file → Normal file
View File

21
components/loadbalancer/include/loadbalancer.h Executable file → Normal file
View File

@@ -9,11 +9,32 @@ extern "C" {
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Operational current limiting mode.
*
* This is owned by the loadbalancer/policy layer. MQTT/REST should only
* request a mode change; they should not persist or re-apply their own mode.
*/
typedef enum
{
LOADBALANCER_LIMIT_MODE_OFF = 0,
LOADBALANCER_LIMIT_MODE_MANUAL,
LOADBALANCER_LIMIT_MODE_GRID,
LOADBALANCER_LIMIT_MODE_SOLAR,
LOADBALANCER_LIMIT_MODE_GRID_SOLAR
} loadbalancer_limit_mode_t;
void loadbalancer_init(void);
void loadbalancer_set_enabled(bool enabled);
bool loadbalancer_is_enabled(void);
// Operational limit mode
esp_err_t loadbalancer_set_limit_mode(loadbalancer_limit_mode_t mode);
loadbalancer_limit_mode_t loadbalancer_get_limit_mode(void);
const char *loadbalancer_limit_mode_to_str(loadbalancer_limit_mode_t mode);
bool loadbalancer_limit_mode_from_str(const char *str, loadbalancer_limit_mode_t *out);
// GRID limit (A)
void loadbalancer_grid_set_enabled(bool en);
bool loadbalancer_grid_is_enabled(void);

View File

@@ -2,9 +2,9 @@
#pragma once
#include "esp_event.h"
#include <stdint.h>
#include <stdbool.h>
#include "esp_timer.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@@ -12,49 +12,59 @@ extern "C" {
ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS);
typedef enum {
typedef enum
{
LOADBALANCER_EVENT_INIT = 0,
LOADBALANCER_EVENT_STATE_CHANGED,
LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT,
// IMPORTANT: eventos separados e payloads diferentes
/*
* Current limit events.
*
* MASTER and SLAVE use different payloads intentionally.
*/
LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
/*
* Status received from slave connectors.
*/
LOADBALANCER_EVENT_SLAVE_STATUS
} loadbalancer_event_id_t;
typedef struct {
bool enabled;
int64_t timestamp_us;
typedef struct
{
bool enabled;
int64_t timestamp_us;
} loadbalancer_state_event_t;
typedef struct {
float limit;
int64_t timestamp_us;
} loadbalancer_global_limit_event_t;
// MASTER: NÃO tem slave_id
typedef struct {
/*
* MASTER: no slave_id.
*/
typedef struct
{
uint16_t max_current;
int64_t timestamp_us;
int64_t timestamp_us;
} loadbalancer_master_limit_event_t;
// SLAVE: tem slave_id
typedef struct {
uint8_t slave_id;
/*
* SLAVE: includes slave_id.
*/
typedef struct
{
uint8_t slave_id;
uint16_t max_current;
int64_t timestamp_us;
int64_t timestamp_us;
} loadbalancer_slave_limit_event_t;
typedef struct {
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
typedef struct
{
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} loadbalancer_slave_status_event_t;
#ifdef __cplusplus
}
#endif
#endif

0
components/loadbalancer/include/pv_optimizer.h Executable file → Normal file
View File

0
components/loadbalancer/src/grid_limiter.c Executable file → Normal file
View File

0
components/loadbalancer/src/input_filter.c Executable file → Normal file
View File

620
components/loadbalancer/src/loadbalancer.c Executable file → Normal file
View File

@@ -18,6 +18,7 @@
#include "input_filter.h"
#include "meter_events.h"
#include "evse_events.h"
#include "evse_config.h"
#include "storage_service.h"
#include "evse_link_events.h"
@@ -42,7 +43,7 @@ static const char *TAG = "loadbalancer";
#define LB_SUSPEND_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT - 1.0f)
#define LB_RESUME_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT + 1.0f)
#define GRID_METER_TIMEOUT_US (10LL * 1000000LL)
#define GRID_METER_TIMEOUT_US (30LL * 1000000LL)
static bool loadbalancer_enabled = false;
@@ -53,6 +54,7 @@ static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; // mantém nome (já u
// PV
static bool pv_enabled = false;
static int32_t pv_max_import_w = 0; // 0=só PV
static loadbalancer_limit_mode_t limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
// métricas do meter
static float grid_current = 0.0f; // fallback magnitude (max irms filtrado)
@@ -100,6 +102,7 @@ static const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL;
#define LB_KEY_GRID_MAXA "grid_max_a"
#define LB_KEY_PV_ON "pv_on"
#define LB_KEY_PV_MAXW "pv_max_w" // precisa storage u32/i32
#define LB_KEY_LIMIT_MODE "limit_mode"
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
@@ -286,9 +289,25 @@ static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t
have_grid_evt = true;
ESP_LOGI(TAG, "GRID: I=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)",
grid_current, (long)grid_watt_total, (unsigned)max_grid_current,
pv_enabled, (long)pv_max_import_w);
if (loadbalancer_enabled)
{
ESP_LOGI(TAG,
"GRID: V=[%.1f %.1f %.1f]V I=[%.3f %.3f %.3f]A maxI=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)",
evt->vrms[0], evt->vrms[1], evt->vrms[2],
evt->irms[0], evt->irms[1], evt->irms[2],
grid_current,
(long)grid_watt_total,
(unsigned)max_grid_current,
pv_enabled,
(long)pv_max_import_w);
}
else
{
ESP_LOGD(TAG,
"GRID ignored because loadbalancer disabled: maxI=%.3fA P=%ldW",
grid_current,
(long)grid_watt_total);
}
}
else
{
@@ -354,6 +373,179 @@ static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t
}
// --------- Config load/save ---------
const char *loadbalancer_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";
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
return "grid_solar";
default:
return "off";
}
}
bool loadbalancer_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 || 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 persist_limit_mode(loadbalancer_limit_mode_t mode)
{
esp_err_t err = storage_set_u8_sync(LB_NS, LB_KEY_LIMIT_MODE, (uint8_t)mode, TO_TICKS_MS(1000));
if (err == ESP_OK)
err = storage_flush_sync(TO_TICKS_MS(2000));
if (err != ESP_OK)
ESP_LOGW(TAG, "Persist %s=%u failed: %s", LB_KEY_LIMIT_MODE, (unsigned)mode, esp_err_to_name(err));
return err;
}
static void loadbalancer_apply_limit_mode_no_persist(loadbalancer_limit_mode_t mode)
{
switch (mode)
{
case LOADBALANCER_LIMIT_MODE_OFF:
loadbalancer_enabled = false;
grid_limit_enabled = false;
pv_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_MANUAL:
loadbalancer_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_GRID:
loadbalancer_enabled = true;
grid_limit_enabled = true;
pv_enabled = false;
grid_limiter_set_enabled(true);
pv_optimizer_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_SOLAR:
loadbalancer_enabled = true;
grid_limit_enabled = false;
pv_enabled = true;
grid_limiter_set_enabled(false);
pv_optimizer_set_enabled(true);
break;
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
loadbalancer_enabled = true;
grid_limit_enabled = true;
pv_enabled = true;
grid_limiter_set_enabled(true);
pv_optimizer_set_enabled(true);
break;
default:
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
loadbalancer_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
return;
}
limit_mode = mode;
}
esp_err_t loadbalancer_set_limit_mode(loadbalancer_limit_mode_t mode)
{
if (mode > LOADBALANCER_LIMIT_MODE_GRID_SOLAR)
return ESP_ERR_INVALID_ARG;
loadbalancer_apply_limit_mode_no_persist(mode);
(void)storage_set_u8_sync(LB_NS, LB_KEY_ENABLED,
loadbalancer_enabled ? 1 : 0,
TO_TICKS_MS(1000));
(void)storage_set_u8_sync(LB_NS, LB_KEY_GRID_ON,
grid_limit_enabled ? 1 : 0,
TO_TICKS_MS(1000));
(void)storage_set_u8_sync(LB_NS, LB_KEY_PV_ON,
pv_enabled ? 1 : 0,
TO_TICKS_MS(1000));
esp_err_t err = persist_limit_mode(mode);
loadbalancer_state_event_t evt = {
.enabled = loadbalancer_enabled,
.timestamp_us = esp_timer_get_time()};
(void)esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_STATE_CHANGED,
&evt,
sizeof(evt),
portMAX_DELAY);
return err;
}
loadbalancer_limit_mode_t loadbalancer_get_limit_mode(void)
{
return limit_mode;
}
static esp_err_t loadbalancer_load_config(void)
{
esp_err_t se = storage_service_init();
@@ -364,72 +556,318 @@ static esp_err_t loadbalancer_load_config(void)
bool needs_flush = false;
uint8_t temp_u8 = 0;
// enabled
/*
* Defaults seguros em RAM.
*
* Estes valores só são gravados na storage quando a chave não existe
* ou quando existe mas tem valor inválido.
*
* Em erro temporário de leitura, usa-se o default em RAM, mas NÃO se
* sobrescreve a storage.
*/
loadbalancer_enabled = false;
grid_limit_enabled = true;
max_grid_current = MAX_GRID_CURRENT_LIMIT;
pv_enabled = false;
pv_max_import_w = 0;
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
/*
* enabled - legacy.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_ENABLED, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
loadbalancer_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
loadbalancer_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_ENABLED);
}
else if (err == ESP_OK)
{
loadbalancer_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=0",
LB_KEY_ENABLED,
(unsigned)temp_u8);
}
else
{
loadbalancer_enabled = false;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_ENABLED,
esp_err_to_name(err));
}
// grid on
/*
* grid_on - legacy/config auxiliar.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_ON, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
grid_limit_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
grid_limit_enabled = true;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=1", LB_KEY_GRID_ON);
}
else if (err == ESP_OK)
{
grid_limit_enabled = true;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=1",
LB_KEY_GRID_ON,
(unsigned)temp_u8);
}
else
{
grid_limit_enabled = true;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=1, storage not overwritten.",
LB_NS,
LB_KEY_GRID_ON,
esp_err_to_name(err));
}
// grid maxA
/*
* grid_max_a.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_MAXA, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT)
if (err == ESP_OK &&
temp_u8 >= MIN_GRID_CURRENT_LIMIT &&
temp_u8 <= MAX_GRID_CURRENT_LIMIT)
{
max_grid_current = temp_u8;
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s missing -> default=%u",
LB_KEY_GRID_MAXA,
(unsigned)max_grid_current);
}
else if (err == ESP_OK)
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=%u",
LB_KEY_GRID_MAXA,
(unsigned)temp_u8,
(unsigned)max_grid_current);
}
else
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=%u, storage not overwritten.",
LB_NS,
LB_KEY_GRID_MAXA,
esp_err_to_name(err),
(unsigned)max_grid_current);
}
// pv on
/*
* pv_on - legacy/config auxiliar.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_PV_ON, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
pv_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
pv_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_PV_ON);
}
else if (err == ESP_OK)
{
pv_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=0",
LB_KEY_PV_ON,
(unsigned)temp_u8);
}
else
{
pv_enabled = false;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_PV_ON,
esp_err_to_name(err));
}
// pv maxW (se não tiveres storage_get_u32_sync, adapta para i32/u32 do teu storage_service)
/*
* pv_max_w.
*/
uint32_t temp_u32 = 0;
err = storage_get_u32_sync(LB_NS, LB_KEY_PV_MAXW, &temp_u32, TO_TICKS_MS(800));
if (err == ESP_OK)
{
pv_max_import_w = (int32_t)temp_u32;
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
pv_max_import_w = 0;
(void)storage_set_u32_async(LB_NS, LB_KEY_PV_MAXW, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_PV_MAXW);
}
else
{
pv_max_import_w = 0;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_PV_MAXW,
esp_err_to_name(err));
}
/*
* limit_mode - fonte principal do estado operacional.
*
* IMPORTANTE:
* - Se a chave existe e é válida, ela ganha sempre.
* - Só migrar dos booleans antigos quando LB_KEY_LIMIT_MODE não existe.
* - Se a leitura falhar temporariamente, NÃO gravar OFF por cima.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_LIMIT_MODE, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= LOADBALANCER_LIMIT_MODE_GRID_SOLAR)
{
limit_mode = (loadbalancer_limit_mode_t)temp_u8;
}
else if (err == ESP_ERR_NOT_FOUND)
{
/*
* Migração de versões antigas sem limit_mode.
*/
if (loadbalancer_enabled && pv_enabled && grid_limit_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_GRID_SOLAR;
}
else if (loadbalancer_enabled && pv_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_SOLAR;
}
else if (loadbalancer_enabled && grid_limit_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_GRID;
}
else
{
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
}
(void)storage_set_u8_async(LB_NS,
LB_KEY_LIMIT_MODE,
(uint8_t)limit_mode);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s missing -> migrated mode=%s",
LB_KEY_LIMIT_MODE,
loadbalancer_limit_mode_to_str(limit_mode));
}
else if (err == ESP_OK)
{
/*
* Valor existente mas inválido.
* Aqui é aceitável reparar para OFF.
*/
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
(void)storage_set_u8_async(LB_NS,
LB_KEY_LIMIT_MODE,
(uint8_t)limit_mode);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> mode=off",
LB_KEY_LIMIT_MODE,
(unsigned)temp_u8);
}
else
{
/*
* Erro temporário de leitura.
* Não sobrescrever storage.
*/
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime mode=off, storage not overwritten.",
LB_NS,
LB_KEY_LIMIT_MODE,
esp_err_to_name(err));
}
if (needs_flush)
(void)storage_flush_sync(TO_TICKS_MS(2000));
{
esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000));
if (fe != ESP_OK)
{
ESP_LOGW(TAG,
"LB config: flush defaults/migration failed: %s",
esp_err_to_name(fe));
}
}
// propagar para sub-módulos
pv_optimizer_set_enabled(pv_enabled);
/*
* Propagar parâmetros para sub-módulos antes de aplicar o modo.
*/
(void)pv_optimizer_set_max_import_w(pv_max_import_w);
grid_limiter_set_enabled(grid_limit_enabled);
(void)grid_limiter_set_max_import_a(max_grid_current);
/*
* Aplicar modo operacional sem persistir novamente.
*/
loadbalancer_apply_limit_mode_no_persist(limit_mode);
ESP_LOGW(TAG,
"LB config loaded: mode=%s enabled=%d grid=%d maxA=%u pv=%d pvMaxW=%ld",
loadbalancer_limit_mode_to_str(limit_mode),
loadbalancer_enabled,
grid_limit_enabled,
(unsigned)max_grid_current,
pv_enabled,
(long)pv_max_import_w);
return ESP_OK;
}
@@ -438,17 +876,19 @@ static void persist_u32(const char *key, uint32_t v) { (void)storage_set_u32_asy
void loadbalancer_set_enabled(bool enabled)
{
if (enabled == loadbalancer_enabled)
return;
loadbalancer_enabled = enabled;
persist_u8(LB_KEY_ENABLED, enabled ? 1 : 0);
loadbalancer_state_event_t evt = {.enabled = enabled, .timestamp_us = esp_timer_get_time()};
(void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_STATE_CHANGED,
&evt, sizeof(evt), portMAX_DELAY);
if (enabled)
{
/*
* Compatibilidade legacy:
* enabled=true passa a significar modo GRID.
*/
(void)loadbalancer_set_limit_mode(LOADBALANCER_LIMIT_MODE_GRID);
}
else
{
(void)loadbalancer_set_limit_mode(LOADBALANCER_LIMIT_MODE_OFF);
}
}
bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; }
// grid setters/getters
@@ -463,8 +903,13 @@ bool loadbalancer_grid_is_enabled(void) { return grid_limit_enabled; }
esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a)
{
ESP_LOGW(TAG, "SET grid max import: %uA -> %uA",
(unsigned)max_grid_current,
(unsigned)a);
if (a < MIN_GRID_CURRENT_LIMIT || a > MAX_GRID_CURRENT_LIMIT)
return ESP_ERR_INVALID_ARG;
max_grid_current = a;
persist_u8(LB_KEY_GRID_MAXA, a);
return grid_limiter_set_max_import_a(a);
@@ -741,83 +1186,60 @@ static void apply_meter_timeout_policy_locked(int *idxs, int cnt)
static void apply_normal_policy_locked(int *idxs, int cnt, float grid_snapshot)
{
// o teu algoritmo atual (grid-limit em A, por corrente medida)
float available = (float)max_grid_current - grid_snapshot;
if (available < -AVAILABLE_EPS)
{
float factor = 0.0f;
if (grid_snapshot > AVAILABLE_EPS)
factor = ((float)max_grid_current) / grid_snapshot;
if (factor < 0.0f)
factor = 0.0f;
if (factor > 1.0f)
factor = 1.0f;
for (int k = 0; k < cnt; k++)
{
int i = idxs[k];
// baseline = runtime (como tinhas)
float cur = connectors[i].runtime_current;
float hw = connectors[i].hw_max_current;
if (cur < 0.0f)
cur = 0.0f;
if (hw < 0.0f)
hw = 0.0f;
connectors[i].assigned = MIN(cur, hw) * factor;
}
return;
}
// baseline = runtime
for (int k = 0; k < cnt; k++)
{
int i = idxs[k];
float cur = connectors[i].runtime_current;
float hw = connectors[i].hw_max_current;
if (cur < 0.0f)
cur = 0.0f;
if (hw < 0.0f)
hw = 0.0f;
connectors[i].assigned = MIN(cur, hw);
}
if (fabsf(available) <= AVAILABLE_EPS)
/*
* Grid limit em modo normal.
*
* max_grid_current é o limite TOTAL de importação da instalação.
*
* Não podemos usar runtime_current como baseline inicial, porque isso
* permite comandos acima do limite configurado. Exemplo observado:
* maxA=8 e SET_CURRENT=22A/27A.
*
* Fórmula:
*
* budget = ultimo_total_comandado + (max_grid_current - grid_medido)
*
* Exemplos:
* last=0A, grid=0A, max=8A => budget=8A
* last=20A, grid=15A, max=8A => budget=13A
* last=0A, grid=5A, max=8A => budget=3A => abaixo de 6A, fica pausado
*/
if (cnt <= 0)
return;
float remaining = available;
if (grid_snapshot < 0.0f)
grid_snapshot = 0.0f;
// fase 1: subir até MIN para os mais antigos, se possível
for (int k = 0; k < cnt && remaining > 0.0f; k++)
{
int i = idxs[k];
float hw = connectors[i].hw_max_current;
float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
const float total_hw_max_a = sum_hw_max_locked(idxs, cnt);
const float last_total_cmd_a = sum_last_limit_locked(idxs, cnt);
if (connectors[i].assigned >= target)
continue;
float budget_total_a =
last_total_cmd_a + ((float)max_grid_current - grid_snapshot);
float delta = target - connectors[i].assigned;
float inc = MIN(delta, remaining);
connectors[i].assigned += inc;
remaining -= inc;
}
if (budget_total_a < 0.0f)
budget_total_a = 0.0f;
// fase 2: suspender os mais recentes que ficaram abaixo do mínimo
for (int k = cnt - 1; k >= 0; k--)
{
int i = idxs[k];
if (connectors[i].assigned >= (float)MIN_CHARGING_CURRENT_LIMIT)
continue;
/*
* Safety clamp:
* Em modo grid-limit, o total comandado nunca deve ultrapassar
* max_grid_current. A fórmula incremental pode subir acima do limite
* quando o meter ainda mede pouca corrente após pausa/retoma.
*/
if (budget_total_a > (float)max_grid_current)
budget_total_a = (float)max_grid_current;
connectors[i].assigned = 0.0f;
connectors[i].suspended_by_lb = true;
}
if (budget_total_a > total_hw_max_a)
budget_total_a = total_hw_max_a;
// fase 3: distribuir extra pelos que estão ativos
if (remaining > AVAILABLE_EPS)
distribute_extra_evenly(idxs, cnt, remaining);
ESP_LOGI(TAG,
"GRID policy: maxA=%u grid=%.2fA lastCmd=%.2fA budget=%.2fA active=%d",
(unsigned)max_grid_current,
grid_snapshot,
last_total_cmd_a,
budget_total_a,
cnt);
apply_budget_policy_locked(idxs, cnt, budget_total_a);
}
static int prepare_pending_limits_locked(const int *idxs,

0
components/loadbalancer/src/pv_optimizer.c Executable file → Normal file
View File