#include "pv_optimizer.h" #include "esp_log.h" #include 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; }