fix evse_link
This commit is contained in:
@@ -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}"
|
||||
|
||||
42
components/loadbalancer/include/grid_limiter.h
Executable file
42
components/loadbalancer/include/grid_limiter.h
Executable 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_ */
|
||||
@@ -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
|
||||
}
|
||||
|
||||
40
components/loadbalancer/include/pv_optimizer.h
Executable file
40
components/loadbalancer/include/pv_optimizer.h
Executable 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_ */
|
||||
123
components/loadbalancer/src/grid_limiter.c
Executable file
123
components/loadbalancer/src/grid_limiter.c
Executable 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
165
components/loadbalancer/src/pv_optimizer.c
Executable file
165
components/loadbalancer/src/pv_optimizer.c
Executable 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;
|
||||
}
|
||||
Reference in New Issue
Block a user