Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion SmartEVSE-3/src/esp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ struct SettingsCache {
uint8_t Switch, RCmon;
uint16_t StartCurrent, StopTime, ImportCurrent;
uint8_t Grid, SB2_WIFImode, RFIDReader;
uint8_t MainsMeterType, MainsMeterAddress, EVMeterType, EVMeterAddress;
uint8_t MainsMeterType, MainsMeterAddress, EVMeterType, EVMeterAddress, CircuitMeterType, CircuitMeterAddress;
uint16_t MaxCircuitMains;
uint8_t EMEndianness, EMIDivisor, EMUDivisor, EMPDivisor, EMEDivisor, EMDataType, EMFunction;
uint16_t EMIRegister, EMURegister, EMPRegister, EMERegister;
uint8_t WIFImode;
Expand Down Expand Up @@ -836,6 +837,28 @@ void mqtt_receive_callback(const String topic, const String payload) {
break;
// END PLAN-09

// BEGIN PLAN-14: CircuitMeter MQTT commands
case MQTT_CMD_MAX_CIRCUIT_MAINS:
if (LoadBl < 2)
MaxCircuitMains = cmd.max_circuit_mains;
break;

case MQTT_CMD_CIRCUIT_METER:
if (CircuitMeter.Type != EM_API || LoadBl >= 2)
return;
#if SMARTEVSE_VERSION < 40
CircuitMeter.setTimeout(COMM_TIMEOUT);
CircuitMeter.Irms[0] = cmd.circuit_meter.L1;
CircuitMeter.Irms[1] = cmd.circuit_meter.L2;
CircuitMeter.Irms[2] = cmd.circuit_meter.L3;
CircuitMeter.CalcImeasured();
#else
Serial1.printf("@Irms:%03u,%d,%d,%d\n", CircuitMeter.Address,
(int)cmd.circuit_meter.L1, (int)cmd.circuit_meter.L2, (int)cmd.circuit_meter.L3);
#endif
break;
// END PLAN-14

// BEGIN PLAN-09: HomeWizard manual IP fallback
case MQTT_CMD_HOMEWIZARD_IP:
homeWizardManualIP = cmd.homewizard_ip;
Expand Down Expand Up @@ -1028,6 +1051,29 @@ void SetupMQTTClient() {
MQTTclient.announce("EV Total Energy Charged", "sensor", optional_payload);
}

// BEGIN PLAN-14: CircuitMeter HA discovery
if (CircuitMeter.Type) {
// Circuit current sensors
optional_payload = MQTTclient.jsna("device_class","current") + MQTTclient.jsna("state_class","measurement") + MQTTclient.jsna("unit_of_measurement","A") + MQTTclient.jsna("value_template", R"({{ value | int / 10 }})");
MQTTclient.announce("Circuit Current L1", "sensor", optional_payload);
MQTTclient.announce("Circuit Current L2", "sensor", optional_payload);
MQTTclient.announce("Circuit Current L3", "sensor", optional_payload);

// Circuit power sensor
optional_payload = MQTTclient.jsna("device_class","power") + MQTTclient.jsna("state_class","measurement") + MQTTclient.jsna("unit_of_measurement","W");
MQTTclient.announce("Circuit Power", "sensor", optional_payload);

// Circuit energy sensors
optional_payload = MQTTclient.jsna("device_class","energy") + MQTTclient.jsna("unit_of_measurement","Wh") + MQTTclient.jsna("state_class","total_increasing");
MQTTclient.announce("Circuit Import Energy", "sensor", optional_payload);
MQTTclient.announce("Circuit Export Energy", "sensor", optional_payload);
}

// MaxCircuitMains number entity (always announced, even when CircuitMeter disabled)
optional_payload = MQTTclient.jsna("device_class","current") + MQTTclient.jsna("unit_of_measurement","A") + MQTTclient.jsna("command_topic", String(MQTTprefix + "/Set/MaxCircuitMains")) + MQTTclient.jsna("min", "0") + MQTTclient.jsna("max", "600") + MQTTclient.jsna("mode","box");
MQTTclient.announce("Max Circuit Mains", "number", optional_payload);
// END PLAN-14

//set the parameters for and MQTTclient.announce sensor entities without device_class or unit_of_measurement:
optional_payload = "";
MQTTclient.announce("EV Plug State", "sensor", optional_payload);
Expand Down Expand Up @@ -1240,6 +1286,19 @@ void mqttPublishData() {
if (EVMeter.EnergyPhase[2] > 0)
mqtt_pub_int(MQTT_SLOT_EV_ENERGY_L3, "/EVEnergyL3", EVMeter.EnergyPhase[2], false, now_s);
}
// BEGIN PLAN-14: CircuitMeter publishing
if (CircuitMeter.Type) {
mqtt_pub_int(MQTT_SLOT_CIRCUIT_L1, "/CircuitCurrentL1", CircuitMeter.Irms[0], false, now_s);
mqtt_pub_int(MQTT_SLOT_CIRCUIT_L2, "/CircuitCurrentL2", CircuitMeter.Irms[1], false, now_s);
mqtt_pub_int(MQTT_SLOT_CIRCUIT_L3, "/CircuitCurrentL3", CircuitMeter.Irms[2], false, now_s);
mqtt_pub_int(MQTT_SLOT_CIRCUIT_POWER, "/CircuitPower", CircuitMeter.PowerMeasured, false, now_s);
if (CircuitMeter.Import_active_energy > 0)
mqtt_pub_int(MQTT_SLOT_CIRCUIT_IMPORT_ENERGY, "/CircuitImportEnergy", CircuitMeter.Import_active_energy, false, now_s);
if (CircuitMeter.Export_active_energy > 0)
mqtt_pub_int(MQTT_SLOT_CIRCUIT_EXPORT_ENERGY, "/CircuitExportEnergy", CircuitMeter.Export_active_energy, false, now_s);
}
mqtt_pub_int(MQTT_SLOT_MAX_CIRCUIT_MAINS, "/MaxCircuitMains", MaxCircuitMains, true, now_s);
// END PLAN-14
mqtt_pub_int(MQTT_SLOT_ESP_TEMP, "/ESPTemp", TempEVSE, false, now_s);
mqtt_pub_str(MQTT_SLOT_MODE, "/Mode", AccessStatus == OFF ? "Off" : AccessStatus == PAUSE ? "Pause" : Mode > 3 ? "N/A" : StrMode[Mode], true, now_s);
mqtt_pub_int(MQTT_SLOT_MAX_CURRENT, "/MaxCurrent", MaxCurrent * 10, true, now_s);
Expand Down Expand Up @@ -1549,6 +1608,9 @@ void read_settings() {
MainsMeter.Address = preferences.getUChar("MainsMAddress",MAINS_METER_ADDRESS);
EVMeter.Type = preferences.getUChar("EVMeter",EV_METER);
EVMeter.Address = preferences.getUChar("EVMeterAddress",EV_METER_ADDRESS);
CircuitMeter.Type = preferences.getUChar("CircuitMeter", CIRCUIT_METER);
CircuitMeter.Address = preferences.getUChar("CirMeterAddr", CIRCUIT_METER_ADDRESS);
MaxCircuitMains = preferences.getUShort("MaxCirMains", MAX_CIRCUIT_MAINS);
EMConfig[EM_CUSTOM].Endianness = preferences.getUChar("EMEndianness",EMCUSTOM_ENDIANESS);
EMConfig[EM_CUSTOM].IRegister = preferences.getUShort("EMIRegister",EMCUSTOM_IREGISTER);
EMConfig[EM_CUSTOM].IDivisor = preferences.getUChar("EMIDivisor",EMCUSTOM_IDIVISOR);
Expand Down Expand Up @@ -1622,6 +1684,9 @@ void read_settings() {
settingsCache.MainsMeterAddress = MainsMeter.Address;
settingsCache.EVMeterType = EVMeter.Type;
settingsCache.EVMeterAddress = EVMeter.Address;
settingsCache.CircuitMeterType = CircuitMeter.Type;
settingsCache.CircuitMeterAddress = CircuitMeter.Address;
settingsCache.MaxCircuitMains = MaxCircuitMains;
settingsCache.EMEndianness = EMConfig[EM_CUSTOM].Endianness;
settingsCache.EMIRegister = EMConfig[EM_CUSTOM].IRegister;
settingsCache.EMIDivisor = EMConfig[EM_CUSTOM].IDivisor;
Expand Down Expand Up @@ -1701,6 +1766,9 @@ void write_settings(void) {
PREFS_PUT_UCHAR_IF_CHANGED("MainsMAddress", MainsMeter.Address, MainsMeterAddress);
PREFS_PUT_UCHAR_IF_CHANGED("EVMeter", EVMeter.Type, EVMeterType);
PREFS_PUT_UCHAR_IF_CHANGED("EVMeterAddress", EVMeter.Address, EVMeterAddress);
PREFS_PUT_UCHAR_IF_CHANGED("CircuitMeter", CircuitMeter.Type, CircuitMeterType);
PREFS_PUT_UCHAR_IF_CHANGED("CirMeterAddr", CircuitMeter.Address, CircuitMeterAddress);
PREFS_PUT_USHORT_IF_CHANGED("MaxCirMains", MaxCircuitMains, MaxCircuitMains);
PREFS_PUT_UCHAR_IF_CHANGED("EMEndianness", EMConfig[EM_CUSTOM].Endianness, EMEndianness);
PREFS_PUT_USHORT_IF_CHANGED("EMIRegister", EMConfig[EM_CUSTOM].IRegister, EMIRegister);
PREFS_PUT_UCHAR_IF_CHANGED("EMIDivisor", EMConfig[EM_CUSTOM].IDivisor, EMIDivisor);
Expand Down
13 changes: 13 additions & 0 deletions SmartEVSE-3/src/evse_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ extern Node_t Node[];
extern Meter MainsMeter;
extern Meter EVMeter;
extern int16_t CapacityHeadroom_da;
extern Meter CircuitMeter;
extern uint16_t MaxCircuitMains;

// These are inside the #if CH32/v3 guard in main.cpp
extern uint8_t C1Timer;
Expand Down Expand Up @@ -328,6 +330,17 @@ void evse_sync_globals_to_ctx(void) {
ctx->MainsMeterTimeout = MainsMeter.Timeout;
ctx->EVMeterTimeout = EVMeter.Timeout;

// CircuitMeter: sync max of 3 phases and configured limit
if (CircuitMeter.Type) {
int16_t cmax = CircuitMeter.Irms[0];
if (CircuitMeter.Irms[1] > cmax) cmax = CircuitMeter.Irms[1];
if (CircuitMeter.Irms[2] > cmax) cmax = CircuitMeter.Irms[2];
ctx->CircuitMeterImeasured = cmax;
} else {
ctx->CircuitMeterImeasured = 0;
}
ctx->MaxCircuitMains = MaxCircuitMains;

ctx->ErrorFlags = ErrorFlags;
ctx->ChargeDelay = ChargeDelay;
ctx->NoCurrent = NoCurrent;
Expand Down
2 changes: 2 additions & 0 deletions SmartEVSE-3/src/evse_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ typedef struct {
uint16_t MaxCurrent;
uint16_t MinCurrent;
uint16_t MaxCircuit;
uint16_t MaxCircuitMains; /* Max current (A) on subpanel circuit, 0 = disabled */
int32_t CircuitMeterImeasured; /* Max per-phase current from circuit meter (dA), 0 when disabled */
uint16_t MaxCapacity;
uint16_t MaxSumMains;
uint8_t MaxSumMainsTime;
Expand Down
25 changes: 25 additions & 0 deletions SmartEVSE-3/src/evse_state_machine.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ void evse_init(evse_ctx_t *ctx, evse_hal_t *hal) {
ctx->MaxCurrent = MAX_CURRENT;
ctx->MinCurrent = MIN_CURRENT;
ctx->MaxCircuit = MAX_CIRCUIT;
ctx->MaxCircuitMains = 0; // Disabled by default
ctx->CircuitMeterImeasured = 0;
ctx->MaxCapacity = MAX_CURRENT; // Default to MaxCurrent
ctx->MaxSumMains = MAX_SUMMAINS;
ctx->MaxSumMainsTime = MAX_SUMMAINSTIME;
Expand Down Expand Up @@ -751,6 +753,13 @@ void evse_calc_balanced_current(evse_ctx_t *ctx, int mod) {
Idifference = cap_diff;
}

/* CircuitMeter subpanel current limit (Plan 14) */
if (ctx->MaxCircuitMains) {
int32_t circuit_headroom = ((int32_t)(ctx->MaxCircuitMains * 10)) - ctx->CircuitMeterImeasured;
if (circuit_headroom < Idifference)
Idifference = circuit_headroom;
}

// Ongoing regulation (lines 1252-1265)
// Issue #15: smart dead band + symmetric ramp rates
// Issue #18: suppress regulation during settling window
Expand Down Expand Up @@ -837,6 +846,12 @@ void evse_calc_balanced_current(evse_ctx_t *ctx, int mod) {
ctx->IsetBalanced = min_int(ctx->IsetBalanced,
(int32_t)ctx->CapacityHeadroom_da / phases);
}
/* CircuitMeter subpanel limit (Plan 14) */
if (ctx->MaxCircuitMains) {
int32_t circuit_phases = evse_force_single_phase(ctx) ? 1 : 3;
ctx->IsetBalanced = min_int(ctx->IsetBalanced,
(((int32_t)(ctx->MaxCircuitMains * 10)) - ctx->CircuitMeterImeasured) / circuit_phases);
}
}
}
}
Expand All @@ -857,6 +872,12 @@ void evse_calc_balanced_current(evse_ctx_t *ctx, int mod) {
ctx->IsetBalanced = min_int(ctx->IsetBalanced,
(int32_t)ctx->CapacityHeadroom_da / phases);
}
/* CircuitMeter guard rail (Plan 14) */
if (ctx->MaxCircuitMains && ctx->Mode != MODE_NORMAL) {
int32_t circuit_phases = evse_force_single_phase(ctx) ? 1 : 3;
ctx->IsetBalanced = min_int(ctx->IsetBalanced,
(((int32_t)(ctx->MaxCircuitMains * 10)) - ctx->CircuitMeterImeasured) / circuit_phases);
}

// ---- Phase 4b: EMA smoothing (Issue #15) ----
// Apply exponential moving average to IsetBalanced to dampen oscillation.
Expand Down Expand Up @@ -929,6 +950,10 @@ void evse_calc_balanced_current(evse_ctx_t *ctx, int mod) {
hardShortage = true;
if (!ctx->MaxSumMainsTime && LimitedByMaxSumMains)
hardShortage = true;
/* CircuitMeter hard shortage (Plan 14) */
if (ctx->MaxCircuitMains &&
ctx->CircuitMeterImeasured > ((int32_t)(ctx->MaxCircuitMains * 10)))
hardShortage = true;

// Priority scheduling: master with multiple EVSEs in shortage
if (ctx->LoadBl == 1 && ActiveEVSE > 1) {
Expand Down
54 changes: 53 additions & 1 deletion SmartEVSE-3/src/http_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ extern capacity_state_t CapacityState;
extern uint16_t MaxCurrent;
extern uint16_t MinCurrent;
extern uint16_t MaxCircuit;
extern uint16_t MaxCircuitMains;
extern uint16_t StartCurrent;
extern uint16_t StopTime;
extern uint16_t ImportCurrent;
Expand Down Expand Up @@ -166,7 +167,7 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR

boolean evConnected = pilot != PILOT_12V; //when access bit = 1, p.ex. in OFF mode, the STATEs are no longer updated

DynamicJsonDocument doc(3700); // https://arduinojson.org/v6/assistant/ (3200 + nodes array)
DynamicJsonDocument doc(4096); // https://arduinojson.org/v6/assistant/ (3200 + nodes + circuit_meter)
doc["version"] = String(VERSION);
doc["serialnr"] = serialnr;
doc["mode"] = mode;
Expand Down Expand Up @@ -223,6 +224,9 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
doc["settings"]["current_main"] = MaxMains;
doc["settings"]["current_max_circuit"] = MaxCircuit;
doc["settings"]["current_max_sum_mains"] = MaxSumMains;
doc["settings"]["circuit_meter_type"] = CircuitMeter.Type;
doc["settings"]["circuit_meter_address"] = CircuitMeter.Address;
doc["settings"]["max_circuit_mains"] = MaxCircuitMains;
doc["settings"]["max_sum_mains_time"] = MaxSumMainsTime;
doc["settings"]["solar_max_import"] = ImportCurrent;
doc["settings"]["solar_start_current"] = StartCurrent;
Expand Down Expand Up @@ -344,6 +348,20 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
doc["mains_meter"]["host"] = !homeWizardHost.isEmpty() ? homeWizardHost : "HomeWizard P1 Not Found";
}

// BEGIN PLAN-14: CircuitMeter data in /settings GET
if (CircuitMeter.Type) {
doc["circuit_meter"]["description"] = EMConfig[CircuitMeter.Type].Desc;
doc["circuit_meter"]["address"] = CircuitMeter.Address;
doc["circuit_meter"]["import_active_energy"] = CircuitMeter.Import_active_energy;
doc["circuit_meter"]["export_active_energy"] = CircuitMeter.Export_active_energy;
doc["circuit_meter"]["power"] = CircuitMeter.PowerMeasured;
doc["circuit_meter"]["currents"]["TOTAL"] = CircuitMeter.Irms[0] + CircuitMeter.Irms[1] + CircuitMeter.Irms[2];
doc["circuit_meter"]["currents"]["L1"] = CircuitMeter.Irms[0];
doc["circuit_meter"]["currents"]["L2"] = CircuitMeter.Irms[1];
doc["circuit_meter"]["currents"]["L3"] = CircuitMeter.Irms[2];
}
// END PLAN-14

doc["phase_currents"]["TOTAL"] = MainsMeter.Irms[0] + MainsMeter.Irms[1] + MainsMeter.Irms[2];
doc["phase_currents"]["L1"] = MainsMeter.Irms[0];
doc["phase_currents"]["L2"] = MainsMeter.Irms[1];
Expand Down Expand Up @@ -431,6 +449,18 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
}
}

// BEGIN PLAN-14: MaxCircuitMains via REST
if(request->hasParam("max_circuit_mains")) {
int current = request->getParam("max_circuit_mains")->value().toInt();
if (LoadBl < 2 && (current == 0 || (current >= 10 && current <= 600))) {
MaxCircuitMains = current;
doc["max_circuit_mains"] = MaxCircuitMains;
} else {
doc["max_circuit_mains"] = "Value not allowed!";
}
}
// END PLAN-14

if(request->hasParam("max_sum_mains_timer")) {
int time = request->getParam("max_sum_mains_timer")->value().toInt();
if(time >= 0 && time <= 60 && LoadBl < 2) {
Expand Down Expand Up @@ -896,6 +926,28 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
}
}

// BEGIN PLAN-14: CircuitMeter API feed via /currents POST
if(CircuitMeter.Type == EM_API) {
if(request->hasParam("circuit_L1") && request->hasParam("circuit_L2") && request->hasParam("circuit_L3")) {
if (LoadBl < 2) {
#if SMARTEVSE_VERSION < 40 //v3
CircuitMeter.Irms[0] = request->getParam("circuit_L1")->value().toInt();
CircuitMeter.Irms[1] = request->getParam("circuit_L2")->value().toInt();
CircuitMeter.Irms[2] = request->getParam("circuit_L3")->value().toInt();
CircuitMeter.CalcImeasured();
CircuitMeter.setTimeout(COMM_TIMEOUT);
#else //v4
Serial1.printf("@Irms:%03u,%d,%d,%d\n", CircuitMeter.Address, (int16_t) request->getParam("circuit_L1")->value().toInt(), (int16_t) request->getParam("circuit_L2")->value().toInt(), (int16_t) request->getParam("circuit_L3")->value().toInt());
#endif
for (int x = 0; x < 3; x++)
doc["circuit"]["L" + x] = CircuitMeter.Irms[x];
doc["circuit"]["TOTAL"] = CircuitMeter.Irms[0] + CircuitMeter.Irms[1] + CircuitMeter.Irms[2];
} else
doc["circuit"]["TOTAL"] = "not allowed on slave";
}
}
// END PLAN-14

String json;
serializeJson(doc, json);
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\r\n", json.c_str()); // Yes. Respond JSON
Expand Down
Loading
Loading