Skip to content

Commit dc3bfc2

Browse files
authored
feat: power input methods — docs, staleness, HW energy, manual IP, diagnostics (#71-#75)
feat: power input methods — docs, staleness, HW energy, manual IP, diagnostics (#71-#75)
2 parents 3bc00d6 + f19af9f commit dc3bfc2

17 files changed

+1325
-31
lines changed

AGENT_PROMPT.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Plan 09 — Power Input Methods: Documentation & Feature Completeness
2+
3+
## Your Role: Network & MQTT Specialist + State Machine Specialist
4+
5+
You are implementing Plan 09: comprehensive documentation for all 5 power input
6+
methods, plus feature gap fixes (API staleness detection, HomeWizard P1 energy data,
7+
manual IP fallback, and diagnostic counters).
8+
9+
## Plan File
10+
11+
Read the full plan at: `/Users/basmeerman/Downloads/EVSE-team-planning/plan-09-power-input-methods.md`
12+
13+
## Branch & Workflow
14+
15+
- **Branch:** `work/plan-09`
16+
- **Remote:** Push to `myfork` (basmeerman/SmartEVSE-3.5), NEVER to `origin`
17+
- **Workflow:** Specification-first for code changes (SbE → test → implement → verify)
18+
- **Verify after each code increment:** `cd SmartEVSE-3/test/native && make clean test`
19+
- **Verify firmware builds:** `pio run -e release -d SmartEVSE-3/`
20+
- **Commit and push after each increment** — do not batch multiple increments into one commit
21+
22+
## GitHub Issues (in order)
23+
24+
1. **#71** — Power Input Methods Documentation (Increment 1) — START HERE
25+
2. **#72** — API/MQTT Staleness Detection (Increment 2)
26+
3. **#73** — HomeWizard P1 Energy Data (Increment 3)
27+
4. **#74** — HomeWizard P1 Manual IP Fallback (Increment 4)
28+
5. **#75** — Diagnostic Counters (Increment 5)
29+
30+
## File Ownership — YOU OWN THESE
31+
32+
### New files (create these):
33+
- `docs/power-input-methods.md` — Comprehensive power input guide (Increment 1)
34+
- `SmartEVSE-3/test/native/tests/test_metering_diagnostics.c` — Diagnostic counter tests (Increment 5)
35+
36+
### Files you modify (YOUR sections only):
37+
- `SmartEVSE-3/src/mqtt_parser.c` / `mqtt_parser.h` — Add staleness timeout parsing,
38+
HomeWizardIP command
39+
- `SmartEVSE-3/src/evse_ctx.h` — Add `api_mains_last_update_ms`, `api_mains_timeout_ms`,
40+
diagnostic counter fields
41+
- `SmartEVSE-3/src/evse_state_machine.c` — Staleness check in tick function (CRITICAL —
42+
must have tests)
43+
- `SmartEVSE-3/test/native/Makefile` — Add test_metering_diagnostics build rule
44+
- `README.md` — Add "Power Input Methods" to documentation table
45+
46+
## SHARED FILES — COORDINATE CAREFULLY
47+
48+
These files are also modified by Plan 06 and/or Plan 07:
49+
50+
- **`network_common.cpp`**: You modify `getMainsFromHomeWizardP1()` to read energy
51+
fields (Increment 3) and add HomeWizardIP manual fallback in mDNS discovery
52+
(Increment 4). Keep your changes in clearly marked sections with comments like
53+
`// BEGIN PLAN-09: HomeWizard energy data` and `// END PLAN-09`.
54+
55+
## Architecture Constraints
56+
57+
1. **CRITICAL: `evse_state_machine.c` changes MUST have tests** — this is safety-critical
58+
2. Staleness detection must use pure C, testable natively
59+
3. Use `snprintf` everywhere, never `sprintf`
60+
4. All test functions MUST have SbE annotations (@feature, @req, @scenario, etc.)
61+
5. Use `test_framework.h` (NOT unity.h)
62+
6. New `evse_ctx.h` fields must be documented with comments
63+
7. Never change existing MQTT topic names — only add new ones
64+
8. Never change existing `/settings` JSON field names — only add new fields
65+
9. Requirement prefix: `REQ-MTR-` for metering, `REQ-MQTT-` for MQTT
66+
67+
## Increment 1 Detailed Steps (Start Here — Documentation Only)
68+
69+
1. Read the plan's Section 1 (Power Input Method Analysis) thoroughly
70+
2. Read existing code to verify the data flow descriptions:
71+
- `main.cpp`: `ModbusRequestLoop()` for Modbus RTU and Sensorbox
72+
- `esp32.cpp`: `homewizard_loop()` / `getMainsFromHomeWizardP1()` for HomeWizard
73+
- `network_common.cpp`: MQTT callback for API/MQTT feed
74+
3. Create `docs/power-input-methods.md` with:
75+
- Reliability ranking table (most reliable first)
76+
- Decision tree flowchart (text-based)
77+
- Per-method setup guide (5 methods)
78+
- Comparison table
79+
- Troubleshooting section
80+
4. Update `README.md` documentation table with link
81+
5. Commit and push
82+
83+
## Increment 2 Detailed Steps (API/MQTT Staleness — Most Important Code Change)
84+
85+
1. Write SbE scenarios:
86+
- Given API metering, when no update for 120s, then fall back to MaxMains
87+
- Given API metering, when update received, then reset staleness timer
88+
- Given API metering, when staleness detected, then set diagnostic flag
89+
- Given non-API metering, then staleness check is skipped
90+
2. Add fields to `evse_ctx.h`: `api_mains_last_update_ms`, `api_mains_timeout_ms`
91+
3. Write tests in existing test file or new test file
92+
4. Implement staleness check in `evse_state_machine.c`
93+
5. Add MQTT parser for timeout setting
94+
6. Run `make clean test`
95+
7. Verify firmware builds
96+
97+
## Key References
98+
99+
- MQTT parser pattern: `src/mqtt_parser.c` (see existing `Set/MainsMeter` handler)
100+
- HomeWizard code: search for `HomeWizard` in `esp32.cpp` and `network_common.cpp`
101+
- Modbus metering: `main.cpp` `ModbusRequestLoop()`
102+
- State machine: `src/evse_state_machine.c`
103+
- Context struct: `src/evse_ctx.h`
104+
- Test framework: `test/native/include/test_framework.h`
105+
- Existing meter tests: `test/native/tests/test_meter_decode.c`

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ Connect to your WiFi network, then browse to `http://smartevse-xxxx.local/update
251251
| Document | Description |
252252
|----------|-------------|
253253
| [Hardware installation](docs/installation.md) | Wiring, mounting, contactor setup |
254+
| [Power Input Methods](docs/power-input-methods.md) | Metering options: Modbus, Sensorbox, HomeWizard, MQTT — reliability ranking, setup, troubleshooting |
254255
| [Configuration](docs/configuration.md) | LCD menu settings reference |
255256
| [Operation](docs/operation.md) | Day-to-day usage guide |
256257
| [Solar & Smart Mode Stability](docs/solar-smart-stability.md) | EMA smoothing, dead bands, phase switch timers, cycling prevention |

SmartEVSE-3/src/esp32.cpp

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,21 @@ void mqtt_receive_callback(const String topic, const String payload) {
816816
break;
817817
// END PLAN-06
818818

819+
// BEGIN PLAN-09: API staleness timeout
820+
case MQTT_CMD_MAINS_METER_TIMEOUT:
821+
// Applied via bridge layer to evse_ctx.api_mains_timeout
822+
break;
823+
// END PLAN-09
824+
825+
// BEGIN PLAN-09: HomeWizard manual IP fallback
826+
case MQTT_CMD_HOMEWIZARD_IP:
827+
homeWizardManualIP = cmd.homewizard_ip;
828+
// Clear cached mDNS result so next poll uses the new IP
829+
homeWizardHost = "";
830+
_LOG_A("HomeWizard manual IP set to '%s'\n", cmd.homewizard_ip);
831+
break;
832+
// END PLAN-09
833+
819834
default:
820835
return;
821836
}
@@ -1020,6 +1035,12 @@ void SetupMQTTClient() {
10201035
MQTTclient.announce("LoadBl", "sensor", optional_payload);
10211036
MQTTclient.announce("PairingPin", "sensor", optional_payload);
10221037
MQTTclient.announce("Firmware Version", "sensor", optional_payload);
1038+
// BEGIN PLAN-09: Metering diagnostic counters
1039+
optional_payload = MQTTclient.jsna("entity_category","diagnostic") + MQTTclient.jsna("state_class","total_increasing") + MQTTclient.jsna("entity_registry_enabled_default","False");
1040+
MQTTclient.announce("MeterTimeoutCount", "sensor", optional_payload);
1041+
MQTTclient.announce("MeterRecoveryCount", "sensor", optional_payload);
1042+
MQTTclient.announce("ApiStaleCount", "sensor", optional_payload);
1043+
// END PLAN-09
10231044

10241045
#if MODEM
10251046
optional_payload = MQTTclient.jsna("unit_of_measurement","%") + MQTTclient.jsna("value_template", R"({{ (value | int / 1024 * 100) | round(0) }})");
@@ -1266,6 +1287,11 @@ void mqttPublishData() {
12661287
// Diagnostic: free heap and MQTT message counter
12671288
mqtt_pub_int(MQTT_SLOT_FREE_HEAP, "/FreeHeap", (int32_t)ESP.getFreeHeap(), false, now_s);
12681289
mqtt_pub_int(MQTT_SLOT_MQTT_MSG_COUNT, "/MQTTMsgCount", (int32_t)MQTTMsgCount, false, now_s);
1290+
// BEGIN PLAN-09: Metering diagnostic counters
1291+
mqtt_pub_int(MQTT_SLOT_METER_TIMEOUT_COUNT, "/MeterTimeoutCount", (int32_t)g_evse_ctx.meter_timeout_count, false, now_s);
1292+
mqtt_pub_int(MQTT_SLOT_METER_RECOVERY_COUNT, "/MeterRecoveryCount", (int32_t)g_evse_ctx.meter_recovery_count, false, now_s);
1293+
mqtt_pub_int(MQTT_SLOT_API_STALE_COUNT, "/ApiStaleCount", (int32_t)g_evse_ctx.api_stale_count, false, now_s);
1294+
// END PLAN-09
12691295

12701296
// MQTT config settings — publish for HA switch/number entity state
12711297
MQTTclient.publish(MQTTprefix + "/MQTTChangeOnly", MQTTChangeOnly ? "1" : "0", true, 0);
@@ -2668,16 +2694,22 @@ bool fwNeedsUpdate(char * version) {
26682694
_LOG_A("homewizard_loop(): start HomeWizrd P1 reading.");
26692695
lastCheck_homewizard = currentTime;
26702696

2671-
const auto currents = getMainsFromHomeWizardP1();
2697+
const auto result = getMainsFromHomeWizardP1();
26722698
#if SMARTEVSE_VERSION < 40 //v3
2673-
for (int i = 0; i < currents.first; i++)
2674-
MainsMeter.Irms[i] = currents.second[i];
2675-
if (currents.first) {
2699+
for (int i = 0; i < result.phases; i++)
2700+
MainsMeter.Irms[i] = result.currents[i];
2701+
if (result.phases) {
2702+
// BEGIN PLAN-09: HomeWizard energy data
2703+
if (result.import_energy_wh > 0)
2704+
MainsMeter.Import_active_energy = result.import_energy_wh;
2705+
if (result.export_energy_wh > 0)
2706+
MainsMeter.Export_active_energy = result.export_energy_wh;
2707+
// END PLAN-09
26762708
CalcIsum();
26772709
MainsMeter.setTimeout(COMM_TIMEOUT);
26782710
}
26792711
#else
2680-
Serial1.printf("@Irms:%03u,%d,%d,%d\n", MainsMeter.Address, currents.second[0], currents.second[1], currents.second[2]); //Irms:011,312,123,124 means: the meter on address 11(dec) has Irms[0] 312 dA, Irms[1] of 123 dA, Irms[2] of 124 dA
2712+
Serial1.printf("@Irms:%03u,%d,%d,%d\n", MainsMeter.Address, result.currents[0], result.currents[1], result.currents[2]); //Irms:011,312,123,124 means: the meter on address 11(dec) has Irms[0] 312 dA, Irms[1] of 123 dA, Irms[2] of 124 dA
26812713
#endif
26822714
}
26832715

SmartEVSE-3/src/evse_ctx.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ typedef enum {
185185
#ifndef SMART_DEADBAND_DEFAULT
186186
#define SMART_DEADBAND_DEFAULT 10 /* 1.0A in deciamps */
187187
#endif
188+
#ifndef API_MAINS_STALENESS_DEFAULT
189+
#define API_MAINS_STALENESS_DEFAULT 120 /* Default staleness timeout in seconds */
190+
#endif
191+
#ifndef EM_API_METER
192+
#define EM_API_METER 9 /* MainsMeterType value for API/MQTT feed (matches EM_API in meter.h) */
193+
#endif
188194
#ifndef RAMP_RATE_DIVISOR_DEFAULT
189195
#define RAMP_RATE_DIVISOR_DEFAULT 4 /* Symmetric /4 for both up and down */
190196
#endif
@@ -350,6 +356,16 @@ typedef struct {
350356
uint8_t MainsMeterTimeout;
351357
uint8_t EVMeterTimeout;
352358

359+
// --- API mains staleness detection ---
360+
uint16_t api_mains_staleness_timer; /* Countdown in seconds, reset on API data arrival */
361+
uint16_t api_mains_timeout; /* Configurable staleness timeout: 0=disabled, default 120s */
362+
bool api_mains_stale; /* true when API mains data is stale */
363+
364+
// --- Metering diagnostic counters ---
365+
uint32_t meter_timeout_count; /* Number of CT_NOCOMM timeout events */
366+
uint32_t meter_recovery_count; /* Number of CT_NOCOMM recovery events */
367+
uint32_t api_stale_count; /* Number of API staleness detection events */
368+
353369
// --- Error handling ---
354370
uint8_t ErrorFlags;
355371
uint8_t ChargeDelay;

SmartEVSE-3/src/evse_state_machine.c

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ void evse_init(evse_ctx_t *ctx, evse_hal_t *hal) {
130130
ctx->MainsMeterTimeout = COMM_TIMEOUT;
131131
ctx->EVMeterTimeout = COMM_EVTIMEOUT;
132132

133+
// API mains staleness detection
134+
ctx->api_mains_staleness_timer = API_MAINS_STALENESS_DEFAULT;
135+
ctx->api_mains_timeout = API_MAINS_STALENESS_DEFAULT;
136+
ctx->api_mains_stale = false;
137+
138+
// Metering diagnostic counters
139+
ctx->meter_timeout_count = 0;
140+
ctx->meter_recovery_count = 0;
141+
ctx->api_stale_count = 0;
142+
133143
// Error handling
134144
ctx->ErrorFlags = NO_ERROR;
135145
ctx->ChargeDelay = 0;
@@ -1568,15 +1578,21 @@ void evse_tick_1s(evse_ctx_t *ctx) {
15681578
// MainsMeter timeout (lines 1732-1755)
15691579
if (ctx->MainsMeterType && ctx->LoadBl < 2) {
15701580
if (ctx->MainsMeterTimeout == 0 && !(ctx->ErrorFlags & CT_NOCOMM) && ctx->Mode != MODE_NORMAL) {
1571-
evse_set_error_flags(ctx, CT_NOCOMM);
1572-
evse_set_power_unavailable(ctx);
1581+
// For API metering with staleness enabled, suppress CT_NOCOMM —
1582+
// the staleness mechanism handles the timeout with graceful fallback.
1583+
if (!(ctx->MainsMeterType == EM_API_METER && ctx->api_mains_timeout > 0)) {
1584+
evse_set_error_flags(ctx, CT_NOCOMM);
1585+
evse_set_power_unavailable(ctx);
1586+
ctx->meter_timeout_count++;
1587+
}
15731588
} else if (ctx->MainsMeterTimeout > 0) {
15741589
ctx->MainsMeterTimeout--;
15751590
}
15761591
} else if (ctx->LoadBl > 1) {
15771592
if (ctx->MainsMeterTimeout == 0 && !(ctx->ErrorFlags & CT_NOCOMM)) {
15781593
evse_set_error_flags(ctx, CT_NOCOMM);
15791594
evse_set_power_unavailable(ctx);
1595+
ctx->meter_timeout_count++;
15801596
} else if (ctx->MainsMeterTimeout > 0) {
15811597
ctx->MainsMeterTimeout--;
15821598
}
@@ -1599,13 +1615,39 @@ void evse_tick_1s(evse_ctx_t *ctx) {
15991615
// CT_NOCOMM recovery (line 1769)
16001616
if ((ctx->ErrorFlags & CT_NOCOMM) && ctx->MainsMeterTimeout > 0) {
16011617
evse_clear_error_flags(ctx, CT_NOCOMM);
1618+
ctx->meter_recovery_count++;
16021619
}
16031620

16041621
// EV_NOCOMM recovery (line 1771)
16051622
if ((ctx->ErrorFlags & EV_NOCOMM) && ctx->EVMeterTimeout > 0) {
16061623
evse_clear_error_flags(ctx, EV_NOCOMM);
16071624
}
16081625

1626+
// API mains staleness detection
1627+
// For API metering (EM_API_METER) with staleness enabled, count down a
1628+
// separate timer. On expiry, fall back to MaxMains (safe limit that still
1629+
// allows charging) instead of CT_NOCOMM (which stops charging entirely).
1630+
if (ctx->MainsMeterType == EM_API_METER && ctx->api_mains_timeout > 0 && ctx->LoadBl < 2) {
1631+
// Recovery: clear stale flag when timer is reset by incoming data
1632+
if (ctx->api_mains_stale && ctx->api_mains_staleness_timer > 0) {
1633+
ctx->api_mains_stale = false;
1634+
}
1635+
// Countdown
1636+
if (ctx->api_mains_staleness_timer > 0) {
1637+
ctx->api_mains_staleness_timer--;
1638+
}
1639+
// On expiry: set stale flag and fall back to MaxMains (once)
1640+
if (ctx->api_mains_staleness_timer == 0 && !ctx->api_mains_stale) {
1641+
ctx->api_mains_stale = true;
1642+
ctx->api_stale_count++;
1643+
for (int i = 0; i < 3; i++) {
1644+
ctx->MainsMeterIrms[i] = (int16_t)(ctx->MaxMains * 10);
1645+
}
1646+
}
1647+
} else if (ctx->MainsMeterType != EM_API_METER) {
1648+
ctx->api_mains_stale = false;
1649+
}
1650+
16091651
// Temperature check (lines 1773-1778)
16101652
if (ctx->TempEVSE > ctx->maxTemp && !(ctx->ErrorFlags & TEMP_HIGH)) {
16111653
evse_set_error_flags(ctx, TEMP_HIGH);

SmartEVSE-3/src/mqtt_parser.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,29 @@ bool mqtt_parse_command(const char *prefix, const char *topic,
148148
&out->ev_meter.L3, &out->ev_meter.W, &out->ev_meter.Wh);
149149
}
150150

151+
/* API mains meter staleness timeout: 0=disabled, 10-3600 seconds */
152+
if (match_topic(prefix, topic, "/Set/MainsMeterTimeout")) {
153+
out->cmd = MQTT_CMD_MAINS_METER_TIMEOUT;
154+
int val = atoi(payload);
155+
if (val == 0 || (val >= 10 && val <= 3600)) {
156+
out->mains_meter_timeout = (uint16_t)val;
157+
return true;
158+
}
159+
return false;
160+
}
161+
162+
/* HomeWizard P1 manual IP: skip mDNS, connect directly. Empty = use mDNS */
163+
if (match_topic(prefix, topic, "/Set/HomeWizardIP")) {
164+
out->cmd = MQTT_CMD_HOMEWIZARD_IP;
165+
size_t len = strlen(payload);
166+
if (len >= sizeof(out->homewizard_ip))
167+
return false;
168+
/* Allow empty string to clear manual IP (re-enable mDNS) */
169+
strncpy(out->homewizard_ip, payload, sizeof(out->homewizard_ip) - 1);
170+
out->homewizard_ip[sizeof(out->homewizard_ip) - 1] = '\0';
171+
return true;
172+
}
173+
151174
/* Home battery current: positive = charging, negative = discharging */
152175
if (match_topic(prefix, topic, "/Set/HomeBatteryCurrent")) {
153176
out->cmd = MQTT_CMD_HOME_BATTERY_CURRENT;

SmartEVSE-3/src/mqtt_parser.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ typedef enum {
3030
MQTT_CMD_MQTT_CHANGE_ONLY,
3131
MQTT_CMD_SOLAR_DEBUG,
3232
MQTT_CMD_DIAG_PROFILE,
33+
MQTT_CMD_MAINS_METER_TIMEOUT,
34+
MQTT_CMD_HOMEWIZARD_IP,
3335
} mqtt_cmd_type_t;
3436

3537
// Mode values matching firmware MODE_NORMAL/MODE_SOLAR/MODE_SMART
@@ -68,6 +70,8 @@ typedef struct {
6870
bool mqtt_change_only; // MQTT_CMD_MQTT_CHANGE_ONLY (0/1)
6971
bool solar_debug; // MQTT_CMD_SOLAR_DEBUG (0/1)
7072
uint8_t diag_profile; // MQTT_CMD_DIAG_PROFILE (0-5)
73+
uint16_t mains_meter_timeout; // MQTT_CMD_MAINS_METER_TIMEOUT (0, 10-3600)
74+
char homewizard_ip[16]; // MQTT_CMD_HOMEWIZARD_IP (IPv4 or empty)
7175
};
7276
} mqtt_command_t;
7377

SmartEVSE-3/src/mqtt_publish.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
extern "C" {
1010
#endif
1111

12-
#define MQTT_CACHE_MAX_SLOTS 84 /* 74 current topics + headroom */
12+
#define MQTT_CACHE_MAX_SLOTS 88 /* 81 current topics + headroom */
1313

1414
/* One slot per published MQTT topic, in mqttPublishData() call order */
1515
typedef enum {
@@ -91,6 +91,9 @@ typedef enum {
9191
MQTT_SLOT_OCPP_TX_ACTIVE,
9292
MQTT_SLOT_OCPP_CURRENT_LIMIT,
9393
MQTT_SLOT_OCPP_SMART_CHARGING,
94+
MQTT_SLOT_METER_TIMEOUT_COUNT,
95+
MQTT_SLOT_METER_RECOVERY_COUNT,
96+
MQTT_SLOT_API_STALE_COUNT,
9497
MQTT_SLOT_COUNT /* must be <= MQTT_CACHE_MAX_SLOTS */
9598
} mqtt_slot_t;
9699

0 commit comments

Comments
 (0)