fix evse_link

This commit is contained in:
2026-01-24 16:56:51 +00:00
parent 023644a887
commit 286028b6a8
54 changed files with 4456 additions and 2632 deletions

View File

@@ -1,5 +1,5 @@
set(srcs
"src/input_filter.c" "src/loadbalancer.c" "src/loadbalancer_events.c"
"src/input_filter.c" "src/loadbalancer.c" "src/pv_optimizer.c" "src/grid_limiter.c" "src/loadbalancer_events.c"
)
idf_component_register(SRCS "${srcs}"

View File

@@ -0,0 +1,42 @@
#ifndef GRID_LIMITER_H_
#define GRID_LIMITER_H_
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void grid_limiter_init(void);
void grid_limiter_set_enabled(bool en);
bool grid_limiter_is_enabled(void);
esp_err_t grid_limiter_set_max_import_a(uint8_t a);
uint8_t grid_limiter_get_max_import_a(void);
/**
* @brief Calcula um novo "total_budget_a" (<= current_total_a) para respeitar max_import_a.
*
* Preferência:
* - Usa watt_total (+import / -export) se existir
* - Caso watt_total==0, usa fallback_grid_current_a (magnitude)
*
* @param grid_evt último evento do GRID
* @param fallback_grid_current_a corrente filtrada (magnitude) como fallback
* @param current_total_a total atual a atribuir aos EVSE (A)
* @return total_budget_a (<= current_total_a)
*/
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a);
#ifdef __cplusplus
}
#endif
#endif /* GRID_LIMITER_H_ */

View File

@@ -9,36 +9,26 @@ extern "C" {
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Inicializa o módulo de load balancer
*/
void loadbalancer_init(void);
/**
* @brief Task contínua do algoritmo de balanceamento
*/
void loadbalancer_task(void *param);
/**
* @brief Ativa ou desativa o load balancing
*/
void loadbalancer_set_enabled(bool value);
/**
* @brief Verifica se o load balancing está ativo
*/
void loadbalancer_set_enabled(bool enabled);
bool loadbalancer_is_enabled(void);
/**
* @brief Define a corrente máxima do grid
*/
esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current);
// GRID limit (A)
void loadbalancer_grid_set_enabled(bool en);
bool loadbalancer_grid_is_enabled(void);
esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a);
uint8_t loadbalancer_grid_get_max_import_a(void);
/**
* @brief Obtém a corrente máxima do grid
*/
uint8_t load_balancing_get_max_grid_current(void);
// PV optimizer (W)
void loadbalancer_pv_set_enabled(bool en);
bool loadbalancer_pv_is_enabled(void);
esp_err_t loadbalancer_pv_set_max_import_w(int32_t w);
int32_t loadbalancer_pv_get_max_import_w(void);
// Aliases legacy (se quiseres manter chamadas antigas)
esp_err_t load_balancing_set_max_grid_current(uint8_t value);
uint8_t load_balancing_get_max_grid_current(void);
#ifdef __cplusplus
}

View File

@@ -0,0 +1,40 @@
#ifndef PV_OPTIMIZER_H_
#define PV_OPTIMIZER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void pv_optimizer_init(void);
void pv_optimizer_set_enabled(bool en);
bool pv_optimizer_is_enabled(void);
esp_err_t pv_optimizer_set_max_import_w(int32_t w);
int32_t pv_optimizer_get_max_import_w(void);
/**
* @brief Calcula o budget TOTAL (A) para todos os EVSEs, para manter importação <= max_import_w.
*
* - max_import_w = 0 => modo "Só PV": tenta manter importação ~0 (só consome quando há exportação).
* - max_import_w > 0 => modo "PV-Grid": permite importar até esse valor.
*
* @param grid_evt Último evento do medidor GRID (watt_total assinado).
* @param last_total_cmd_a Soma da corrente comandada no ciclo anterior (A).
* @param total_hw_max_a Soma dos hw_max_current dos conectores ativos (A).
* @return budget_total_a (0..total_hw_max_a)
*/
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a);
#ifdef __cplusplus
}
#endif
#endif /* PV_OPTIMIZER_H_ */

View File

@@ -0,0 +1,123 @@
#include "grid_limiter.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "grid_limiter";
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
uint8_t max_import_a;
} grid_cfg_t;
static grid_cfg_t s_cfg = {
.enabled = false,
.max_import_a = 32};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void grid_limiter_init(void) { /* nada */ }
void grid_limiter_set_enabled(bool en) { s_cfg.enabled = en; }
bool grid_limiter_is_enabled(void) { return s_cfg.enabled; }
esp_err_t grid_limiter_set_max_import_a(uint8_t a)
{
if (a < 6 || a > 100)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_a = a;
return ESP_OK;
}
uint8_t grid_limiter_get_max_import_a(void) { return s_cfg.max_import_a; }
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a)
{
if (!s_cfg.enabled)
return current_total_a;
if (current_total_a <= 0.0f)
return 0.0f;
float i_import = 0.0f;
if (grid_evt && grid_evt->watt_total > 0)
{
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float denom = v_avg * (float)nph * pf;
if (denom > 10.0f)
{
i_import = ((float)grid_evt->watt_total) / denom;
}
else
{
i_import = fallback_grid_current_a;
}
}
else
{
// export (<=0) => import=0; ou sem potência => fallback
if (grid_evt && grid_evt->watt_total < 0)
i_import = 0.0f;
else
i_import = fallback_grid_current_a;
}
if (i_import <= (float)s_cfg.max_import_a + 0.01f)
return current_total_a;
const float over = i_import - (float)s_cfg.max_import_a;
const float cut_a = ceilf(over); // conservador
float new_total = current_total_a - cut_a;
if (new_total < 0.0f)
new_total = 0.0f;
ESP_LOGD(TAG, "cap: i_import=%.2fA max=%uA over=%.2fA total=%.1fA -> %.1fA",
i_import, (unsigned)s_cfg.max_import_a, over, current_total_a, new_total);
return new_total;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
#include "pv_optimizer.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "pv_optimizer";
// internos (fixos, como pediste)
#define PV_MIN_EXPORT_W (50) // deadband export (anti-oscilações)
#define PV_TOTAL_RAMP_STEP_A (2.0f) // step total por ciclo (como tens loop 5s)
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
int32_t max_import_w; // >=0
} pv_cfg_t;
static pv_cfg_t s_cfg = {
.enabled = false,
.max_import_w = 0};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void pv_optimizer_init(void)
{
// nada a fazer
}
void pv_optimizer_set_enabled(bool en) { s_cfg.enabled = en; }
bool pv_optimizer_is_enabled(void) { return s_cfg.enabled; }
esp_err_t pv_optimizer_set_max_import_w(int32_t w)
{
if (w < 0)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_w = w;
return ESP_OK;
}
int32_t pv_optimizer_get_max_import_w(void) { return s_cfg.max_import_w; }
static float ramp_total(float last_a, float target_a)
{
if (target_a > last_a + PV_TOTAL_RAMP_STEP_A)
return last_a + PV_TOTAL_RAMP_STEP_A;
if (target_a < last_a - PV_TOTAL_RAMP_STEP_A)
return last_a - PV_TOTAL_RAMP_STEP_A;
return target_a;
}
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a)
{
if (!s_cfg.enabled)
return total_hw_max_a;
if (!grid_evt)
return 0.0f;
// se meter não fornece potência (fica 0) não dá para PV -> conservador: não importa
// (podes mudar para "mantém last" se preferires)
if (grid_evt->watt_total == 0)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float w_per_a = v_avg * (float)nph * pf;
if (w_per_a < 10.0f)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
const int32_t p_grid_w = grid_evt->watt_total; // +import / -export
const int32_t target_import_w = s_cfg.max_import_w; // >=0
// deadband só para o "Só PV"
if (target_import_w == 0)
{
if (p_grid_w < 0)
{
int32_t export_w = -p_grid_w;
if (export_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
else
{
// está a importar
if (p_grid_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
}
// estima base-load com o comando anterior
const float p_evse_last_w = last_total_cmd_a * w_per_a;
const float p_base_w = (float)p_grid_w - p_evse_last_w;
// queremos p_grid -> target_import_w
float p_evse_target_w = (float)target_import_w - p_base_w;
// clamp [0..max]
if (p_evse_target_w < 0.0f)
p_evse_target_w = 0.0f;
const float p_evse_max_w = total_hw_max_a * w_per_a;
if (p_evse_target_w > p_evse_max_w)
p_evse_target_w = p_evse_max_w;
float target_total_a = p_evse_target_w / w_per_a;
if (target_total_a < 0.0f)
target_total_a = 0.0f;
if (target_total_a > total_hw_max_a)
target_total_a = total_hw_max_a;
float ramped = ramp_total(last_total_cmd_a, target_total_a);
ESP_LOGD(TAG, "pv: p_grid=%ldW target_imp=%ldW base=%.1fW last=%.1fA -> target=%.1fA (v=%.1f nph=%d pf=%.2f)",
(long)p_grid_w, (long)target_import_w, p_base_w, last_total_cmd_a, ramped, v_avg, nph, pf);
return ramped;
}