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/protocols/CMakeLists.txt Executable file → Normal file
View File

17
components/protocols/include/mqtt.h Executable file → Normal file
View File

@@ -16,30 +16,17 @@ extern "C"
#define MQTT_USERNAME_MAX_LEN 32
#define MQTT_PASSWORD_MAX_LEN 64
// =============================
// Limit modes (cmd/evse/limit)
// =============================
typedef enum
{
LIMIT_MODE_OFF = 0, ///< sem limite, usa max do hardware
LIMIT_MODE_MANUAL, ///< corrente fixa definida pelo utilizador
LIMIT_MODE_LOADBALANCER, ///< limita pelo contador da instalação
LIMIT_MODE_SOLAR ///< só com excedente FV
} limit_mode_t;
/**
* @brief Inicializa o módulo MQTT.
*
* - Carrega configuração da NVS
* - Regista handlers de eventos (AUTH, SCHED, LOADBALANCER, METER, EVSE, NETWORK)
* - Cria a task de telemetria periódica
* - Aplica o modo de limite persistido
*
* Não inicia a ligação ao broker imediatamente; isso acontece quando existir IP
* (NETWORK_EVENT_STA_GOT_IP) e `enabled == true`.
*
* IMPORTANTE: chamar DEPOIS de loadbalancer_init() para o restauro do modo
* de limite funcionar correctamente.
* IMPORTANTE: chamar DEPOIS de loadbalancer_init() para o estado publicado
* refletir a política local já restaurada.
*/
esp_err_t mqtt_init(void);

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

252
components/protocols/src/mqtt.c Executable file → Normal file
View File

@@ -55,7 +55,6 @@ static const char *TAG = "mqtt";
#define MQTT_KEY_USER "user"
#define MQTT_KEY_PASS "pass"
#define MQTT_KEY_PERIOD "period"
#define MQTT_KEY_LIMITMODE "limitmode"
#define MQTT_HOST_MAX_LEN 64
#define MQTT_TOPIC_MAX_LEN 64
@@ -73,7 +72,6 @@ static char s_mqtt_user[MQTT_USER_MAX_LEN] = {0};
static char s_mqtt_pass[MQTT_PASS_MAX_LEN] = {0};
static uint16_t s_mqtt_periodicity = 30;
static limit_mode_t s_limit_mode = LIMIT_MODE_OFF;
static char s_client_id[MQTT_CLIENT_ID_MAX_LEN] = {0};
static bool s_session_was_active = false;
@@ -93,10 +91,6 @@ static void mqtt_limit_publish_state(void);
static void mqtt_session_publish_state(void);
static void mqtt_system_publish_heartbeat(void);
static void mqtt_apply_limit_mode(limit_mode_t mode);
static const char *limit_mode_to_str(limit_mode_t m);
static bool limit_mode_from_str(const char *s, limit_mode_t *out);
static void mqtt_subscribe_all(void);
static void mqtt_dispatch_command(const char *topic, const char *data);
@@ -209,25 +203,12 @@ static void mqtt_load_config_from_storage(void)
}
}
{
uint8_t u8 = 0;
err = storage_get_u8_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, &u8, TO_TICKS_MS(500));
if (err == ESP_OK && u8 <= LIMIT_MODE_SOLAR)
s_limit_mode = (limit_mode_t)u8;
else
{
s_limit_mode = LIMIT_MODE_OFF;
(void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, 0);
needs_flush = true;
}
}
if (needs_flush)
(void)storage_flush_async();
ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s' limitmode=%s",
ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s'",
s_mqtt_enabled, s_mqtt_host, s_mqtt_base_topic,
(unsigned)s_mqtt_periodicity, s_mqtt_user, limit_mode_to_str(s_limit_mode));
(unsigned)s_mqtt_periodicity, s_mqtt_user);
}
static esp_err_t mqtt_save_config_to_storage(void)
@@ -262,10 +243,6 @@ static esp_err_t mqtt_save_config_to_storage(void)
if (e != ESP_OK)
err = e;
e = storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)s_limit_mode);
if (e != ESP_OK)
err = e;
(void)storage_flush_async();
if (err != ESP_OK)
@@ -310,85 +287,6 @@ void mqtt_get_password(char *password)
uint16_t mqtt_get_periodicity(void) { return s_mqtt_periodicity; }
static const char *limit_mode_to_str(limit_mode_t m)
{
switch (m)
{
case LIMIT_MODE_OFF:
return "off";
case LIMIT_MODE_MANUAL:
return "manual";
case LIMIT_MODE_LOADBALANCER:
return "loadbalancer";
case LIMIT_MODE_SOLAR:
return "solar";
default:
return "off";
}
}
static bool limit_mode_from_str(const char *s, limit_mode_t *out)
{
if (!s || !out)
return false;
if (strcmp(s, "off") == 0)
{
*out = LIMIT_MODE_OFF;
return true;
}
if (strcmp(s, "manual") == 0)
{
*out = LIMIT_MODE_MANUAL;
return true;
}
if (strcmp(s, "loadbalancer") == 0)
{
*out = LIMIT_MODE_LOADBALANCER;
return true;
}
if (strcmp(s, "solar") == 0)
{
*out = LIMIT_MODE_SOLAR;
return true;
}
return false;
}
static void mqtt_apply_limit_mode(limit_mode_t mode)
{
switch (mode)
{
case LIMIT_MODE_OFF:
loadbalancer_pv_set_enabled(false);
loadbalancer_grid_set_enabled(false);
loadbalancer_set_enabled(false);
evse_set_charging_current(evse_get_max_charging_current());
break;
case LIMIT_MODE_MANUAL:
loadbalancer_pv_set_enabled(false);
loadbalancer_grid_set_enabled(false);
loadbalancer_set_enabled(false);
break;
case LIMIT_MODE_LOADBALANCER:
loadbalancer_pv_set_enabled(false);
loadbalancer_grid_set_enabled(true);
loadbalancer_set_enabled(true);
break;
case LIMIT_MODE_SOLAR:
loadbalancer_grid_set_enabled(false);
loadbalancer_pv_set_enabled(true);
loadbalancer_set_enabled(true);
break;
}
s_limit_mode = mode;
(void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)mode);
(void)storage_flush_async();
}
static void mqtt_build_topic(char *out, size_t out_len, const char *suffix)
{
const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse";
@@ -715,7 +613,7 @@ static void mqtt_limit_publish_state(void)
if (!root)
return;
cJSON_AddStringToObject(root, "mode", limit_mode_to_str(s_limit_mode));
cJSON_AddStringToObject(root, "mode", loadbalancer_limit_mode_to_str(loadbalancer_get_limit_mode()));
cJSON_AddNumberToObject(root, "requestedCurrentA", (int)evse_get_charging_current());
cJSON_AddNumberToObject(root, "effectiveCurrentA", (int)evse_get_runtime_charging_current());
cJSON_AddNumberToObject(root, "hardwareMaxA", (int)evse_get_max_charging_current());
@@ -824,7 +722,6 @@ static void mqtt_loadbalancer_event_handler(void *arg, esp_event_base_t base, in
{
case LOADBALANCER_EVENT_INIT:
case LOADBALANCER_EVENT_STATE_CHANGED:
case LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT:
mqtt_loadbalancer_publish_state();
mqtt_limit_publish_state();
break;
@@ -1158,8 +1055,8 @@ static void handle_cmd_evse_limit(const char *payload)
return;
}
limit_mode_t mode;
if (!limit_mode_from_str(j_mode->valuestring, &mode))
loadbalancer_limit_mode_t mode;
if (!loadbalancer_limit_mode_from_str(j_mode->valuestring, &mode))
{
ESP_LOGW(TAG, "evse/limit: unknown mode '%s'", j_mode->valuestring);
cJSON_Delete(json);
@@ -1168,7 +1065,7 @@ static void handle_cmd_evse_limit(const char *payload)
switch (mode)
{
case LIMIT_MODE_MANUAL:
case LOADBALANCER_LIMIT_MODE_MANUAL:
{
cJSON *j_curr = cJSON_GetObjectItem(json, "requestedCurrentA");
if (!j_curr)
@@ -1183,62 +1080,172 @@ static void handle_cmd_evse_limit(const char *payload)
int a = j_curr->valueint;
uint8_t hw_max = evse_get_max_charging_current();
if (a < MIN_CHARGING_CURRENT_LIMIT || a > hw_max)
{
ESP_LOGW(TAG, "evse/limit manual: currentA %d out of range [%d,%u]",
a, MIN_CHARGING_CURRENT_LIMIT, (unsigned)hw_max);
ESP_LOGW(TAG,
"evse/limit manual: currentA %d out of range [%d,%u]",
a,
MIN_CHARGING_CURRENT_LIMIT,
(unsigned)hw_max);
cJSON_Delete(json);
return;
}
esp_err_t e = evse_set_charging_current((uint16_t)a);
if (e != ESP_OK)
{
ESP_LOGW(TAG,
"evse_set_charging_current failed: %s",
esp_err_to_name(e));
cJSON_Delete(json);
return;
}
evse_set_charging_current((uint16_t)a);
break;
}
case LIMIT_MODE_LOADBALANCER:
case LOADBALANCER_LIMIT_MODE_GRID:
{
cJSON *j = cJSON_GetObjectItem(json, "gridMaxImportA");
if (cJSON_IsNumber(j))
{
int a = j->valueint;
if (a < 6 || a > 100)
{
ESP_LOGW(TAG, "evse/limit loadbalancer: gridMaxImportA %d out of range", a);
ESP_LOGW(TAG,
"evse/limit grid: gridMaxImportA %d out of range",
a);
cJSON_Delete(json);
return;
}
esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)a);
if (e != ESP_OK)
ESP_LOGW(TAG, "loadbalancer_grid_set_max_import_a failed: %s", esp_err_to_name(e));
{
ESP_LOGW(TAG,
"loadbalancer_grid_set_max_import_a failed: %s",
esp_err_to_name(e));
cJSON_Delete(json);
return;
}
}
break;
}
case LIMIT_MODE_SOLAR:
case LOADBALANCER_LIMIT_MODE_SOLAR:
{
cJSON *j = cJSON_GetObjectItem(json, "maxImportW");
if (cJSON_IsNumber(j))
{
int w = j->valueint;
if (w < 0)
{
ESP_LOGW(TAG, "evse/limit solar: maxImportW %d invalid", w);
ESP_LOGW(TAG,
"evse/limit solar: maxImportW %d invalid",
w);
cJSON_Delete(json);
return;
}
esp_err_t e = loadbalancer_pv_set_max_import_w(w);
if (e != ESP_OK)
ESP_LOGW(TAG, "loadbalancer_pv_set_max_import_w failed: %s", esp_err_to_name(e));
{
ESP_LOGW(TAG,
"loadbalancer_pv_set_max_import_w failed: %s",
esp_err_to_name(e));
cJSON_Delete(json);
return;
}
}
break;
}
case LIMIT_MODE_OFF:
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
{
/*
* grid_solar = solar optimizer + grid import cap.
*
* Legacy MQTT flat payload:
* {
* "mode": "grid_solar",
* "gridMaxImportA": 30,
* "maxImportW": 0
* }
*/
cJSON *j_grid = cJSON_GetObjectItem(json, "gridMaxImportA");
if (cJSON_IsNumber(j_grid))
{
int a = j_grid->valueint;
if (a < 6 || a > 100)
{
ESP_LOGW(TAG,
"evse/limit grid_solar: gridMaxImportA %d out of range",
a);
cJSON_Delete(json);
return;
}
esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)a);
if (e != ESP_OK)
{
ESP_LOGW(TAG,
"loadbalancer_grid_set_max_import_a failed: %s",
esp_err_to_name(e));
cJSON_Delete(json);
return;
}
}
cJSON *j_pv = cJSON_GetObjectItem(json, "maxImportW");
if (cJSON_IsNumber(j_pv))
{
int w = j_pv->valueint;
if (w < 0)
{
ESP_LOGW(TAG,
"evse/limit grid_solar: maxImportW %d invalid",
w);
cJSON_Delete(json);
return;
}
esp_err_t e = loadbalancer_pv_set_max_import_w(w);
if (e != ESP_OK)
{
ESP_LOGW(TAG,
"loadbalancer_pv_set_max_import_w failed: %s",
esp_err_to_name(e));
cJSON_Delete(json);
return;
}
}
break;
}
ESP_LOGI(TAG, "MQTT: applying limit mode = %s", limit_mode_to_str(mode));
mqtt_apply_limit_mode(mode);
case LOADBALANCER_LIMIT_MODE_OFF:
break;
}
ESP_LOGI(TAG,
"MQTT: requested limit mode = %s",
loadbalancer_limit_mode_to_str(mode));
esp_err_t mode_err = loadbalancer_set_limit_mode(mode);
if (mode_err != ESP_OK)
ESP_LOGW(TAG,
"loadbalancer_set_limit_mode failed: %s",
esp_err_to_name(mode_err));
cJSON_Delete(json);
mqtt_loadbalancer_publish_state();
@@ -1543,7 +1550,8 @@ esp_err_t mqtt_init(void)
xTaskCreate(mqtt_telemetry_task, "mqtt_telemetry_task", 4096, NULL, 3, &s_telemetry_task);
}
mqtt_apply_limit_mode(s_limit_mode);
ESP_LOGI(TAG, "MQTT init: loadbalancer owns limit mode; MQTT will only apply broker commands");
return ESP_OK;
}

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