Skip to content

Commit 4aabc01

Browse files
basmeermanclaude
andcommitted
feat: solar debug MQTT publishing with rate limiting (#66)
Add MQTT topic SmartEVSE/<serial>/Debug/Solar that publishes a JSON snapshot of the solar regulation state every 5 seconds. Gated behind Set/SolarDebug (1/0) — no overhead when disabled. New pure C module solar_debug_json.c formats evse_solar_debug_t to JSON, tested natively (7 tests). Bridge getter evse_get_solar_debug() reads the snapshot with spinlock protection. MQTT parser extended with MQTT_CMD_SOLAR_DEBUG (3 parser tests). Closes #66 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a97be6c commit 4aabc01

File tree

11 files changed

+383
-0
lines changed

11 files changed

+383
-0
lines changed

SmartEVSE-3/src/esp32.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ char RequiredEVCCID[32] = ""; // R
5050
#include "modbus.h"
5151
#include "meter.h"
5252
#include "evse_bridge.h"
53+
#include "solar_debug_json.h"
5354
#include "mqtt_parser.h"
5455
#include "mqtt_publish.h"
5556
#include "http_api.h"
@@ -798,6 +799,10 @@ void mqtt_receive_callback(const String topic, const String payload) {
798799
request_write_settings();
799800
break;
800801

802+
case MQTT_CMD_SOLAR_DEBUG:
803+
mqttSetSolarDebug(cmd.solar_debug);
804+
break;
805+
801806
default:
802807
return;
803808
}
@@ -1273,6 +1278,36 @@ void mqttSmartEVSEPublishData() {
12731278
MQTTclientSmartEVSE.publish(MQTTSmartEVSEprefix + "/PairingPin", PairingPin, true, 0);
12741279
MQTTclientSmartEVSE.publish(MQTTSmartEVSEprefix + "/MaxCurrent", String(MaxCurrent * 10), true, 0);
12751280
}
1281+
1282+
// Solar debug MQTT publishing (Issue #66)
1283+
// Gated behind SolarDebugEnabled flag — only publishes when enabled.
1284+
// Rate-limited: publishes at most once every SOLAR_DEBUG_INTERVAL_MS.
1285+
static bool SolarDebugEnabled = false;
1286+
static unsigned long SolarDebugLastPublish = 0;
1287+
#define SOLAR_DEBUG_INTERVAL_MS 5000
1288+
1289+
void mqttPublishSolarDebug(void) {
1290+
if (!SolarDebugEnabled) return;
1291+
if (!MQTTclient.connected) return;
1292+
1293+
unsigned long now = millis();
1294+
if (SolarDebugLastPublish != 0 && (now - SolarDebugLastPublish) < SOLAR_DEBUG_INTERVAL_MS)
1295+
return;
1296+
1297+
evse_solar_debug_t snap;
1298+
evse_get_solar_debug(&snap);
1299+
1300+
char json[384];
1301+
if (solar_debug_to_json(&snap, json, sizeof(json)) > 0) {
1302+
MQTTclient.publish(MQTTprefix + "/Debug/Solar", json, false, 0);
1303+
SolarDebugLastPublish = now;
1304+
}
1305+
}
1306+
1307+
void mqttSetSolarDebug(bool enabled) {
1308+
SolarDebugEnabled = enabled;
1309+
if (!enabled) SolarDebugLastPublish = 0;
1310+
}
12761311
#endif
12771312

12781313

SmartEVSE-3/src/evse_bridge.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,18 @@ void evse_bridge_unlock(void) {
482482
#endif
483483
}
484484

485+
// ---- Solar debug snapshot reader (spinlock-protected) ----
486+
void evse_get_solar_debug(evse_solar_debug_t *out) {
487+
if (!out) return;
488+
#ifdef SMARTEVSE_VERSION
489+
portENTER_CRITICAL(&evse_sync_spinlock);
490+
#endif
491+
*out = g_evse_ctx.solar_debug;
492+
#ifdef SMARTEVSE_VERSION
493+
portEXIT_CRITICAL(&evse_sync_spinlock);
494+
#endif
495+
}
496+
485497
// ---- Initialization ----
486498
void evse_bridge_init(void) {
487499
#ifdef SMARTEVSE_VERSION

SmartEVSE-3/src/evse_bridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ void evse_sync_globals_to_ctx(void);
2222
void evse_sync_ctx_to_globals(void);
2323
void evse_bridge_lock(void);
2424
void evse_bridge_unlock(void);
25+
void evse_get_solar_debug(evse_solar_debug_t *out);
2526

2627
#ifdef __cplusplus
2728
}

SmartEVSE-3/src/main.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ extern void BroadcastCurrent(void);
325325
extern void CheckRFID(void);
326326
extern void mqttPublishData();
327327
extern void mqttSmartEVSEPublishData();
328+
extern void mqttPublishSolarDebug(void);
329+
extern void mqttSetSolarDebug(bool enabled);
328330
extern bool MQTTclientSmartEVSE_AppConnected;
329331
extern void DisconnectEvent(void);
330332
extern char EVCCID[32];
@@ -1110,6 +1112,8 @@ static void timer1s_mqtt_publish(void) {
11101112
lastSmartEVSEUpdate = 0;
11111113
mqttSmartEVSEPublishData();
11121114
}
1115+
// Solar debug publishing — rate-limited internally (5s default)
1116+
mqttPublishSolarDebug();
11131117
}
11141118
#endif
11151119

SmartEVSE-3/src/mqtt_parser.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,18 @@ bool mqtt_parse_command(const char *prefix, const char *topic,
264264
return false;
265265
}
266266

267+
if (match_topic(prefix, topic, "/Set/SolarDebug")) {
268+
out->cmd = MQTT_CMD_SOLAR_DEBUG;
269+
if (strcmp(payload, "1") == 0 || strcmp(payload, "ON") == 0) {
270+
out->solar_debug = true;
271+
return true;
272+
}
273+
if (strcmp(payload, "0") == 0 || strcmp(payload, "OFF") == 0) {
274+
out->solar_debug = false;
275+
return true;
276+
}
277+
return false;
278+
}
279+
267280
return false;
268281
}

SmartEVSE-3/src/mqtt_parser.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ typedef enum {
2828
MQTT_CMD_IDLE_TIMEOUT,
2929
MQTT_CMD_MQTT_HEARTBEAT,
3030
MQTT_CMD_MQTT_CHANGE_ONLY,
31+
MQTT_CMD_SOLAR_DEBUG,
3132
} mqtt_cmd_type_t;
3233

3334
// Mode values matching firmware MODE_NORMAL/MODE_SOLAR/MODE_SMART
@@ -64,6 +65,7 @@ typedef struct {
6465
uint16_t idle_timeout; // MQTT_CMD_IDLE_TIMEOUT (30-300)
6566
uint16_t mqtt_heartbeat; // MQTT_CMD_MQTT_HEARTBEAT (10-300)
6667
bool mqtt_change_only; // MQTT_CMD_MQTT_CHANGE_ONLY (0/1)
68+
bool solar_debug; // MQTT_CMD_SOLAR_DEBUG (0/1)
6769
};
6870
} mqtt_command_t;
6971

SmartEVSE-3/src/solar_debug_json.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* solar_debug_json.c - Format evse_solar_debug_t as JSON
3+
*
4+
* Pure C module — no platform dependencies, testable natively.
5+
*/
6+
7+
#include "solar_debug_json.h"
8+
#include <stdio.h>
9+
10+
int solar_debug_to_json(const evse_solar_debug_t *snap, char *buf, size_t bufsz)
11+
{
12+
if (!snap || !buf || bufsz == 0)
13+
return -1;
14+
15+
int n = snprintf(buf, bufsz,
16+
"{"
17+
"\"IsetBalanced\":%d,"
18+
"\"IsetBalanced_ema\":%d,"
19+
"\"Idifference\":%d,"
20+
"\"IsumImport\":%d,"
21+
"\"Isum\":%d,"
22+
"\"MainsMeterImeasured\":%d,"
23+
"\"Balanced0\":%u,"
24+
"\"SolarStopTimer\":%u,"
25+
"\"PhaseSwitchTimer\":%u,"
26+
"\"PhaseSwitchHoldDown\":%u,"
27+
"\"NoCurrent\":%u,"
28+
"\"SettlingTimer\":%u,"
29+
"\"Nr_Of_Phases_Charging\":%u,"
30+
"\"ErrorFlags\":%u"
31+
"}",
32+
(int)snap->IsetBalanced,
33+
(int)snap->IsetBalanced_ema,
34+
(int)snap->Idifference,
35+
(int)snap->IsumImport,
36+
(int)snap->Isum,
37+
(int)snap->MainsMeterImeasured,
38+
(unsigned)snap->Balanced0,
39+
(unsigned)snap->SolarStopTimer,
40+
(unsigned)snap->PhaseSwitchTimer,
41+
(unsigned)snap->PhaseSwitchHoldDown,
42+
(unsigned)snap->NoCurrent,
43+
(unsigned)snap->SettlingTimer,
44+
(unsigned)snap->Nr_Of_Phases_Charging,
45+
(unsigned)snap->ErrorFlags);
46+
47+
if (n < 0 || (size_t)n >= bufsz)
48+
return -1;
49+
50+
return n;
51+
}

SmartEVSE-3/src/solar_debug_json.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* solar_debug_json.h - Format evse_solar_debug_t as JSON
3+
*
4+
* Pure C module — no platform dependencies, testable natively.
5+
*/
6+
7+
#ifndef SOLAR_DEBUG_JSON_H
8+
#define SOLAR_DEBUG_JSON_H
9+
10+
#include "evse_ctx.h"
11+
#include <stddef.h>
12+
13+
#ifdef __cplusplus
14+
extern "C" {
15+
#endif
16+
17+
/**
18+
* Format a solar debug snapshot as a JSON object string.
19+
*
20+
* @param snap Pointer to the solar debug snapshot
21+
* @param buf Output buffer
22+
* @param bufsz Size of output buffer
23+
* @return Number of characters written (excluding NUL), or -1 on error
24+
*/
25+
int solar_debug_to_json(const evse_solar_debug_t *snap, char *buf, size_t bufsz);
26+
27+
#ifdef __cplusplus
28+
}
29+
#endif
30+
31+
#endif /* SOLAR_DEBUG_JSON_H */

SmartEVSE-3/test/native/Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ P1_PARSE_SRC := $(SRC_DIR)/p1_parse.c
3131
MODBUS_LOG_SRC := $(SRC_DIR)/modbus_log.c
3232
OCPP_LOGIC_SRC := $(SRC_DIR)/ocpp_logic.c
3333
OCPP_TELEMETRY_SRC := $(SRC_DIR)/ocpp_telemetry.c
34+
SOLAR_DEBUG_JSON_SRC := $(SRC_DIR)/solar_debug_json.c
3435

3536
# Discover all test files
3637
TEST_SRCS := $(wildcard $(TEST_DIR)/test_*.c)
@@ -93,6 +94,9 @@ $(BUILD)/test_ocpp_telemetry: $(TEST_DIR)/test_ocpp_telemetry.c $(OCPP_TELEMETRY
9394
$(BUILD)/test_ocpp_iec61851: $(TEST_DIR)/test_ocpp_iec61851.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
9495
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_iec61851.c $(OCPP_LOGIC_SRC)
9596

97+
$(BUILD)/test_solar_debug_json: $(TEST_DIR)/test_solar_debug_json.c $(SOLAR_DEBUG_JSON_SRC) $(SRC_DIR)/solar_debug_json.h include/*.h | $(BUILD)
98+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_solar_debug_json.c $(SOLAR_DEBUG_JSON_SRC)
99+
96100
# Build each state machine test binary (generic rule)
97101
$(BUILD)/test_%: $(TEST_DIR)/test_%.c $(EVSE_SRC) include/*.h $(SRC_DIR)/*.h | $(BUILD)
98102
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_$*.c $(EVSE_SRC)

SmartEVSE-3/test/native/tests/test_mqtt_parser.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,48 @@ void test_mqtt_change_only_invalid(void) {
901901
TEST_ASSERT_FALSE(mqtt_parse_command(PREFIX, PREFIX "/Set/MQTTChangeOnly", "2", &cmd));
902902
}
903903

904+
// ---- SolarDebug ----
905+
906+
/*
907+
* @feature Solar Debug Telemetry
908+
* @req REQ-SOL-020
909+
* @scenario SolarDebug enable via MQTT
910+
* @given A valid MQTT prefix
911+
* @when Topic is prefix/Set/SolarDebug with payload "1"
912+
* @then The parser returns true with solar_debug = true
913+
*/
914+
void test_solar_debug_enable(void) {
915+
TEST_ASSERT_TRUE(mqtt_parse_command(PREFIX, PREFIX "/Set/SolarDebug", "1", &cmd));
916+
TEST_ASSERT_EQUAL(MQTT_CMD_SOLAR_DEBUG, cmd.cmd);
917+
TEST_ASSERT_TRUE(cmd.solar_debug);
918+
}
919+
920+
/*
921+
* @feature Solar Debug Telemetry
922+
* @req REQ-SOL-020
923+
* @scenario SolarDebug disable via MQTT
924+
* @given A valid MQTT prefix
925+
* @when Topic is prefix/Set/SolarDebug with payload "0"
926+
* @then The parser returns true with solar_debug = false
927+
*/
928+
void test_solar_debug_disable(void) {
929+
TEST_ASSERT_TRUE(mqtt_parse_command(PREFIX, PREFIX "/Set/SolarDebug", "0", &cmd));
930+
TEST_ASSERT_EQUAL(MQTT_CMD_SOLAR_DEBUG, cmd.cmd);
931+
TEST_ASSERT_FALSE(cmd.solar_debug);
932+
}
933+
934+
/*
935+
* @feature Solar Debug Telemetry
936+
* @req REQ-SOL-020
937+
* @scenario SolarDebug rejects invalid payload
938+
* @given A valid MQTT prefix
939+
* @when Topic is prefix/Set/SolarDebug with payload "2"
940+
* @then The parser returns false
941+
*/
942+
void test_solar_debug_invalid(void) {
943+
TEST_ASSERT_FALSE(mqtt_parse_command(PREFIX, PREFIX "/Set/SolarDebug", "2", &cmd));
944+
}
945+
904946
// ---- Unrecognized topic ----
905947

906948
/*
@@ -1035,6 +1077,11 @@ int main(void) {
10351077
RUN_TEST(test_mqtt_change_only_disable);
10361078
RUN_TEST(test_mqtt_change_only_invalid);
10371079

1080+
// SolarDebug
1081+
RUN_TEST(test_solar_debug_enable);
1082+
RUN_TEST(test_solar_debug_disable);
1083+
RUN_TEST(test_solar_debug_invalid);
1084+
10381085
// Unrecognized
10391086
RUN_TEST(test_unrecognized_topic);
10401087
RUN_TEST(test_wrong_prefix);

0 commit comments

Comments
 (0)