Add human-readable UTC time alongside epoch in web UI and CSV
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
return false;
|
||||
auto parse_float_field = [](const char *start, const char *end, float &out) -> bool {
|
||||
if (!start) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
memcpy(p_buf, p_start, p_len);
|
||||
p_buf[p_len] = '\0';
|
||||
char *endp = nullptr;
|
||||
float p = strtof(p_buf, &endp);
|
||||
if (endp == p_buf) {
|
||||
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>";
|
||||
|
||||
Reference in New Issue
Block a user