Files
chargeflow/components/meter_manager/driver/meter_modbus/meter_ea777.c

762 lines
21 KiB
C
Executable File

// meter_ea777.c — Driver Modbus RTU para EARU EA777 (ESP-IDF)
// Suporta 1 ou 2 EA777 no mesmo bus RS485:
// GRID -> slave ID 1
// EVSE -> slave ID 2
// Usa um único Modbus master e uma única task de leitura.
#include "meter_ea777.h"
#include "meter_events.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stddef.h>
#include <string.h>
#include <math.h>
#include <inttypes.h>
#define TAG "serial_mdb_ea777"
// ===== UART / RS-485 =====
#define MB_PORT_NUM 1
#define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware (evite GPIO2 para RTS/DE/RE se possível)
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ===== EA777 no mesmo bus =====
#define EA777_GRID_SLAVE_ID 1
#define EA777_EVSE_SLAVE_ID 2
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (200 / portTICK_PERIOD_MS)
#define EA777_LOCK_TIMEOUT pdMS_TO_TICKS(10000)
// ===== Helpers =====
#define STR(fieldname) ((const char *)(fieldname))
#define OPTS(min_val, max_val, step_val) {.opt1 = min_val, .opt2 = max_val, .opt3 = step_val}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// ===== Estado =====
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static SemaphoreHandle_t ea777_lock = NULL;
static inline uint32_t ea777_swap_words_u32(uint32_t x)
{
return ((x & 0xFFFFu) << 16) | ((x >> 16) & 0xFFFFu);
}
// ============================================================================
// ============ MAPA DE REGISTROS EA777 (Holding 0x03) ========================
// Endereços zero-based. Tipos reais (engenharia) via fator de escala.
// Tensões (0.1 V)
#define EA777_L1VOLTAGE 0x0000
#define EA777_L2VOLTAGE 0x0001
#define EA777_L3VOLTAGE 0x0002
// Correntes (0.01 A)
#define EA777_L1CURRENT 0x0003
#define EA777_L2CURRENT 0x0004
#define EA777_L3CURRENT 0x0005
// Potência ativa total (W)
#define EA777_TOTAL_ACTIVE_P 0x0007
// Fator de potência por fase (0.001)
#define EA777_PF_L1 0x0014
#define EA777_PF_L2 0x0015
#define EA777_PF_L3 0x0016
// Frequência (0.01 Hz)
#define EA777_FREQUENCY 0x001A
// Energia ativa total (U32 * 0.01 kWh, 2 registradores)
#define EA777_TOTAL_ACTIVE_E 0x001D
// ============================================================================
// ============ CIDs locais por meter ============
enum
{
CID_EA777_L1_VOLTAGE = 0,
CID_EA777_L2_VOLTAGE,
CID_EA777_L3_VOLTAGE,
CID_EA777_L1_CURRENT,
CID_EA777_L2_CURRENT,
CID_EA777_L3_CURRENT,
CID_EA777_TOTAL_ACTIVE_P,
CID_EA777_PF_L1,
CID_EA777_PF_L2,
CID_EA777_PF_L3,
CID_EA777_FREQUENCY,
CID_EA777_TOTAL_ACTIVE_E,
CID_EA777_COUNT,
};
typedef enum
{
EA777_SLOT_GRID = 0,
EA777_SLOT_EVSE,
EA777_MAX_INSTANCES
} ea777_slot_t;
typedef struct
{
bool registered;
const char *source; // "GRID" ou "EVSE"
uint8_t slave_id; // 1 ou 2
uint16_t cid_base; // atribuído dinamicamente em ea777_rebuild_descriptors_locked()
} ea777_instance_t;
static ea777_instance_t ea777_instances[EA777_MAX_INSTANCES] = {
[EA777_SLOT_GRID] = {
.registered = false,
.source = "GRID",
.slave_id = EA777_GRID_SLAVE_ID,
.cid_base = 0,
},
[EA777_SLOT_EVSE] = {
.registered = false,
.source = "EVSE",
.slave_id = EA777_EVSE_SLAVE_ID,
.cid_base = 0,
},
};
// ======= Template de descritores (Holding registers) =======
// Nota: param_offset = 0 -> não usamos holding_reg_params_t aqui.
static const mb_parameter_descriptor_t ea777_param_template[CID_EA777_COUNT] = {
// Tensões (0.1 V)
{CID_EA777_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L1VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
{CID_EA777_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L2VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
{CID_EA777_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L3VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
// Correntes (0.01 A)
{CID_EA777_L1_CURRENT, STR("L1 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L1CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
{CID_EA777_L2_CURRENT, STR("L2 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L2CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
{CID_EA777_L3_CURRENT, STR("L3 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L3CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Potência ativa total (W)
{CID_EA777_TOTAL_ACTIVE_P, STR("Total Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, EA777_TOTAL_ACTIVE_P, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 60000, 1), PAR_PERMS_READ},
// Fator de potência (0.001)
{CID_EA777_PF_L1, STR("L1 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
{CID_EA777_PF_L2, STR("L2 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
{CID_EA777_PF_L3, STR("L3 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
// Frequência (0.01 Hz)
{CID_EA777_FREQUENCY, STR("Frequency"), STR("Hz"), 1,
MB_PARAM_HOLDING, EA777_FREQUENCY, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Energia ativa total (U32 * 0.01 kWh, 2 regs)
{CID_EA777_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1,
MB_PARAM_HOLDING, EA777_TOTAL_ACTIVE_E, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
// O esp-modbus exige cid e param_key únicos no Data Dictionary.
// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância.
static const char *const ea777_param_keys[EA777_MAX_INSTANCES][CID_EA777_COUNT] = {
[EA777_SLOT_GRID] = {
"GRID L1 Voltage",
"GRID L2 Voltage",
"GRID L3 Voltage",
"GRID L1 Current",
"GRID L2 Current",
"GRID L3 Current",
"GRID Total Active Power",
"GRID L1 PF",
"GRID L2 PF",
"GRID L3 PF",
"GRID Frequency",
"GRID Total Active Energy",
},
[EA777_SLOT_EVSE] = {
"EVSE L1 Voltage",
"EVSE L2 Voltage",
"EVSE L3 Voltage",
"EVSE L1 Current",
"EVSE L2 Current",
"EVSE L3 Current",
"EVSE Total Active Power",
"EVSE L1 PF",
"EVSE L2 PF",
"EVSE L3 PF",
"EVSE Frequency",
"EVSE Total Active Energy",
},
};
static mb_parameter_descriptor_t device_parameters_ea777[EA777_MAX_INSTANCES * CID_EA777_COUNT];
static uint16_t num_device_parameters_ea777 = 0;
static bool ea777_descriptor_dirty = true;
static bool ea777_has_registered_instance_locked(void)
{
for (uint8_t i = 0; i < EA777_MAX_INSTANCES; ++i)
{
if (ea777_instances[i].registered)
return true;
}
return false;
}
static esp_err_t ea777_rebuild_descriptors_locked(void)
{
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
for (uint8_t inst_idx = 0; inst_idx < EA777_MAX_INSTANCES; ++inst_idx)
{
ea777_instance_t *inst = &ea777_instances[inst_idx];
if (!inst->registered)
continue;
// Mantém os CIDs contíguos no descriptor ativo.
// Isto evita problemas em versões do esp-modbus que tratam CID como índice.
inst->cid_base = num_device_parameters_ea777;
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; ++local_cid)
{
mb_parameter_descriptor_t *dst = &device_parameters_ea777[num_device_parameters_ea777++];
*dst = ea777_param_template[local_cid];
dst->cid = inst->cid_base + local_cid;
dst->param_key = ea777_param_keys[inst_idx][local_cid];
dst->mb_slave_addr = inst->slave_id;
}
}
ea777_descriptor_dirty = true;
ESP_LOGI(TAG, "EA777 descriptor table prepared: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_apply_descriptors_locked(void)
{
if (num_device_parameters_ea777 == 0)
return ESP_ERR_INVALID_STATE;
if (!ea777_descriptor_dirty)
return ESP_OK;
esp_err_t err = mbc_master_set_descriptor(device_parameters_ea777, num_device_parameters_ea777);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err));
return err;
}
ea777_descriptor_dirty = false;
ESP_LOGI(TAG, "EA777 descriptor table applied: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_master_init_once(void)
{
if (is_initialized)
return ESP_OK;
if (!ea777_lock)
{
ea777_lock = xSemaphoreCreateMutex();
if (!ea777_lock)
return ESP_ERR_NO_MEM;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted before EA777 init");
}
(void)mbc_master_destroy();
ESP_LOGI(TAG, "meter_ea777 Modbus master init");
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_EVEN,
};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
return err;
err = mbc_master_setup(&comm);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// RS-485 half duplex
err = uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
is_initialized = true;
ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)");
return ESP_OK;
}
static esp_err_t ea777_register_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return ESP_ERR_INVALID_ARG;
esp_err_t err = ea777_master_init_once();
if (err != ESP_OK)
return err;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
ea777_instances[slot].registered = true;
ESP_LOGI(TAG, "EA777 %s registered on Modbus slave ID %u",
ea777_instances[slot].source,
ea777_instances[slot].slave_id);
err = ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
return err;
}
static void ea777_shutdown_if_idle(void)
{
bool any_registered = false;
if (ea777_lock && xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
any_registered = ea777_has_registered_instance_locked();
xSemaphoreGive(ea777_lock);
}
if (any_registered || !is_initialized)
return;
ESP_LOGI(TAG, "No EA777 instances registered; stopping shared Modbus master");
task_should_run = false;
for (int i = 0; i < 20 && meter_task != NULL; ++i)
{
vTaskDelay(pdMS_TO_TICKS(10));
}
if (meter_task != NULL)
{
ESP_LOGW(TAG, "EA777 task did not exit in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err));
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
}
static void ea777_unregister_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return;
if (!is_initialized || !ea777_lock)
return;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
ESP_LOGI(TAG, "EA777 %s unregistered", ea777_instances[slot].source);
ea777_instances[slot].registered = false;
(void)ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
}
ea777_shutdown_if_idle();
}
static void meter_ea777_post_event(const ea777_instance_t *inst,
const float *voltage,
const float *current,
const int32_t *power_w,
int32_t total_power_w,
float freq_hz,
float pf_avg,
float total_kwh)
{
meter_event_data_t evt = {
.source = inst->source,
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh,
.watt_total = total_power_w,
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "%s falha ao emitir evento: %s", inst->source, esp_err_to_name(err));
}
}
static void ea777_read_instance_locked(const ea777_instance_t *inst)
{
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float v[3] = {0};
float i[3] = {0};
float pf[3] = {0};
float freq = 0.0f;
float total_kwh = 0.0f;
int32_t total_active_power_w = 0;
bool got_any_value = false;
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; local_cid++)
{
const uint16_t cid = inst->cid_base + local_cid;
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err));
continue;
}
uint8_t type = 0;
uint16_t raw16 = 0;
uint32_t raw32 = 0;
void *value_ptr = (local_cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16;
// 1 retry simples em caso de timeout
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_ERR_TIMEOUT)
{
vTaskDelay(pdMS_TO_TICKS(60));
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
}
if (err == ESP_OK)
{
got_any_value = true;
switch (local_cid)
{
case CID_EA777_L1_VOLTAGE:
v[0] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L2_VOLTAGE:
v[1] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L3_VOLTAGE:
v[2] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L1_CURRENT:
i[0] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L2_CURRENT:
i[1] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L3_CURRENT:
i[2] = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_P:
total_active_power_w = (int32_t)raw16;
break;
case CID_EA777_PF_L1:
pf[0] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L2:
pf[1] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L3:
pf[2] = ((float)raw16) * 0.001f;
break;
case CID_EA777_FREQUENCY:
freq = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_E:
{
uint32_t raw_e = ea777_swap_words_u32(raw32);
total_kwh = ((float)raw_e) * 0.01f;
ESP_LOGI(TAG,
"%s EA777 energy raw=0x%08" PRIX32
" swapped=0x%08" PRIX32 " => %.3f kWh",
inst->source,
raw32,
raw_e,
(double)total_kwh);
break;
}
default:
break;
}
ESP_LOGD(TAG, "%s %s (cid=%u) -> raw16=%u raw32=%u",
inst->source, desc->param_key, cid,
(unsigned int)raw16,
(unsigned int)raw32);
}
else
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
inst->source, cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
if (!got_any_value)
{
ESP_LOGW(TAG, "%s no valid EA777 values read in this cycle", inst->source);
return;
}
// Potência por fase aproximada: P = V * I * PF.
// O EA777 também fornece potência ativa total; esta é usada em watt_total quando existe.
int32_t p_int[3] = {
(int32_t)lrintf(v[0] * i[0] * pf[0]),
(int32_t)lrintf(v[1] * i[1] * pf[1]),
(int32_t)lrintf(v[2] * i[2] * pf[2]),
};
const int32_t p_sum = p_int[0] + p_int[1] + p_int[2];
const int32_t watt_total = (total_active_power_w != 0) ? total_active_power_w : p_sum;
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
meter_ea777_post_event(inst, v, i, p_int, watt_total, freq, pf_avg, total_kwh);
}
// ===== Task de polling partilhada =====
static void serial_mdb_ea777_task(void *param)
{
(void)param;
// pequeno settle antes da 1ª leitura
vTaskDelay(pdMS_TO_TICKS(200));
while (task_should_run)
{
if (!is_initialized || !ea777_lock)
{
vTaskDelay(UPDATE_INTERVAL);
continue;
}
bool had_instance = false;
for (uint8_t slot = 0; slot < EA777_MAX_INSTANCES && task_should_run; ++slot)
{
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
{
ESP_LOGW(TAG, "EA777 task timeout waiting lock");
continue;
}
ea777_instance_t inst = ea777_instances[slot];
if (inst.registered)
{
had_instance = true;
ea777_read_instance_locked(&inst);
}
xSemaphoreGive(ea777_lock);
}
vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "EA777 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
// ===== API pública =====
// Compatibilidade: chamada antiga inicializa EA777 como GRID/ID 1.
esp_err_t meter_ea777_init(void)
{
return meter_ea777_grid_init();
}
esp_err_t meter_ea777_grid_init(void)
{
return ea777_register_instance(EA777_SLOT_GRID);
}
esp_err_t meter_ea777_evse_init(void)
{
return ea777_register_instance(EA777_SLOT_EVSE);
}
esp_err_t meter_ea777_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "meter_ea777 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!ea777_lock)
return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
const bool has_instance = ea777_has_registered_instance_locked();
esp_err_t desc_err = ESP_OK;
if (has_instance)
desc_err = ea777_apply_descriptors_locked();
xSemaphoreGive(ea777_lock);
if (!has_instance)
{
ESP_LOGW(TAG, "meter_ea777 start ignored: no instances registered");
return ESP_ERR_INVALID_STATE;
}
if (desc_err != ESP_OK)
return desc_err;
if (meter_task == NULL)
{
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_ea777_task,
"meter_ea777_task",
4096, NULL, 3, &meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_ea777 shared task started");
}
return ESP_OK;
}
esp_err_t meter_ea777_grid_start(void)
{
return meter_ea777_start();
}
esp_err_t meter_ea777_evse_start(void)
{
return meter_ea777_start();
}
void meter_ea777_stop(void)
{
meter_ea777_grid_stop();
}
void meter_ea777_grid_stop(void)
{
ea777_unregister_instance(EA777_SLOT_GRID);
}
void meter_ea777_evse_stop(void)
{
ea777_unregister_instance(EA777_SLOT_EVSE);
}