Add human-readable UTC time alongside epoch in web UI and CSV

This commit is contained in:
2026-02-16 08:57:16 +01:00
parent 0a2e4e5a68
commit 4de1dda82b
2 changed files with 58 additions and 21 deletions

View File

@@ -39,6 +39,21 @@ static String format_date_utc(uint32_t ts_utc) {
return String(buf);
}
static String format_hms_utc(uint32_t ts_utc) {
if (ts_utc == 0) {
return "";
}
time_t t = static_cast<time_t>(ts_utc);
struct tm tm_utc;
gmtime_r(&t, &tm_utc);
char buf[16];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d",
tm_utc.tm_hour,
tm_utc.tm_min,
tm_utc.tm_sec);
return String(buf);
}
void sd_logger_init() {
if (!ENABLE_SD_LOGGING) {
g_sd_ready = false;
@@ -87,11 +102,14 @@ void sd_logger_log_sample(const MeterData &data, bool include_error_text) {
}
if (new_file) {
f.println("ts_utc,p_w,p1_w,p2_w,p3_w,e_kwh,bat_v,bat_pct,rssi,snr,err_m,err_d,err_tx,err_last");
f.println("ts_utc,ts_hms_utc,p_w,p1_w,p2_w,p3_w,e_kwh,bat_v,bat_pct,rssi,snr,err_m,err_d,err_tx,err_last");
}
String ts_hms_utc = format_hms_utc(data.ts_utc);
f.print(data.ts_utc);
f.print(',');
f.print(ts_hms_utc);
f.print(',');
f.print(data.total_power_w, 1);
f.print(',');
f.print(data.phase_power_w[0], 1);

View File

@@ -57,24 +57,28 @@ static HistoryJob g_history = {};
static constexpr size_t SD_LIST_MAX_FILES = 200;
static constexpr size_t SD_DOWNLOAD_MAX_PATH = 160;
static String format_utc_timestamp(uint32_t ts_utc) {
static String format_utc_hms(uint32_t ts_utc) {
if (ts_utc == 0) {
return "n/a";
}
time_t t = static_cast<time_t>(ts_utc);
struct tm tm_utc;
gmtime_r(&t, &tm_utc);
char buf[32];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d UTC",
tm_utc.tm_year + 1900,
tm_utc.tm_mon + 1,
tm_utc.tm_mday,
char buf[16];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d UTC",
tm_utc.tm_hour,
tm_utc.tm_min,
tm_utc.tm_sec);
return String(buf);
}
static String format_epoch_hms(uint32_t ts_utc) {
if (ts_utc == 0) {
return "n/a";
}
return String(ts_utc) + " (" + format_utc_hms(ts_utc) + ")";
}
static uint32_t timestamp_age_seconds(uint32_t ts_utc) {
uint32_t now_utc = time_get_utc();
if (ts_utc == 0 || now_utc < ts_utc) {
@@ -289,18 +293,32 @@ static bool history_parse_line(const char *line, uint32_t &ts_out, float &p_out)
if (end == ts_buf) {
return false;
}
const char *p_start = comma + 1;
const char *p_end = strchr(p_start, ',');
char p_buf[16];
size_t p_len = p_end ? static_cast<size_t>(p_end - p_start) : strlen(p_start);
if (p_len >= sizeof(p_buf)) {
auto parse_float_field = [](const char *start, const char *end, float &out) -> bool {
if (!start) {
return false;
}
memcpy(p_buf, p_start, p_len);
char p_buf[16];
size_t p_len = end ? static_cast<size_t>(end - start) : strlen(start);
if (p_len == 0 || p_len >= sizeof(p_buf)) {
return false;
}
memcpy(p_buf, start, p_len);
p_buf[p_len] = '\0';
char *endp = nullptr;
float p = strtof(p_buf, &endp);
if (endp == p_buf) {
out = strtof(p_buf, &endp);
return endp != p_buf;
};
const char *field2_start = comma + 1;
const char *field2_end = strchr(field2_start, ',');
float p = 0.0f;
bool parsed_power = parse_float_field(field2_start, field2_end, p);
if (!parsed_power && field2_end) {
const char *field3_start = field2_end + 1;
const char *field3_end = strchr(field3_start, ',');
parsed_power = parse_float_field(field3_start, field3_end, p);
}
if (!parsed_power) {
return false;
}
ts_out = ts;
@@ -400,7 +418,7 @@ static String render_sender_block(const SenderStatus &status) {
if (!status.has_data) {
s += "No data";
} else {
s += "Last update: " + format_utc_timestamp(status.last_update_ts_utc);
s += "Last update: " + format_epoch_hms(status.last_update_ts_utc);
if (time_is_synced()) {
s += " (" + String(timestamp_age_seconds(status.last_update_ts_utc)) + "s ago)";
}
@@ -419,7 +437,7 @@ static String render_sender_block(const SenderStatus &status) {
duplicate_pct = (static_cast<float>(duplicate_batches) * 100.0f) / static_cast<float>(total_batches);
}
s += "<br>Dup batches: " + String(duplicate_batches) + "/" + String(total_batches) + " (" + String(duplicate_pct, 1) + "%)";
s += " last: " + format_utc_timestamp(status.rx_last_duplicate_ts_utc);
s += " last: " + format_epoch_hms(status.rx_last_duplicate_ts_utc);
if (time_is_synced() && status.rx_last_duplicate_ts_utc > 0) {
s += " (" + String(timestamp_age_seconds(status.rx_last_duplicate_ts_utc)) + "s ago)";
}
@@ -647,13 +665,14 @@ static void handle_sender() {
if (g_last_batch_count[i] > 0) {
html += "<h3>Last batch (" + String(g_last_batch_count[i]) + " samples)</h3>";
html += "<table border='1' cellspacing='0' cellpadding='3'>";
html += "<tr><th>#</th><th>ts</th><th>e_kwh</th><th>p_w</th><th>p1_w</th><th>p2_w</th><th>p3_w</th>";
html += "<tr><th>#</th><th>ts_utc</th><th>ts_hms_utc</th><th>e_kwh</th><th>p_w</th><th>p1_w</th><th>p2_w</th><th>p3_w</th>";
html += "<th>bat_v</th><th>bat_pct</th><th>rssi</th><th>snr</th><th>err_tx</th><th>err_last</th><th>rx_reject</th></tr>";
for (uint8_t r = 0; r < g_last_batch_count[i]; ++r) {
const MeterData &d = g_last_batch[i][r];
html += "<tr>";
html += "<td>" + String(r) + "</td>";
html += "<td>" + String(d.ts_utc) + "</td>";
html += "<td>" + format_utc_hms(d.ts_utc) + "</td>";
html += "<td>" + String(d.energy_total_kwh, 2) + "</td>";
html += "<td>" + String(round_power_w(d.total_power_w)) + "</td>";
html += "<td>" + String(round_power_w(d.phase_power_w[0])) + "</td>";