Files
chargeflow/components/loadbalancer/src/pv_optimizer.c

166 lines
4.1 KiB
C

#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;
}