Skip to content

Commit b6193b9

Browse files
Copilotdorkmo
andcommitted
Add Blues VIN voltage reporting to daily report and dashboards
Co-authored-by: dorkmo <[email protected]>
1 parent 9a4fe1e commit b6193b9

File tree

3 files changed

+145
-8
lines changed

3 files changed

+145
-8
lines changed

TankAlarm-112025-Client-BluesOpta/TankAlarm-112025-Client-BluesOpta.ino

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ static void setRelayState(uint8_t relayNum, bool state);
277277
static void initializeRelays();
278278
static void triggerRemoteRelays(const char *targetClient, uint8_t relayMask, bool activate);
279279
static int getRelayPin(uint8_t relayIndex);
280+
static float readNotecardVinVoltage();
280281

281282
void setup() {
282283
Serial.begin(115200);
@@ -1368,6 +1369,9 @@ static void sendDailyReport() {
13681369
uint8_t part = 0;
13691370
bool queuedAny = false;
13701371

1372+
// Read VIN voltage once for the daily report
1373+
float vinVoltage = readNotecardVinVoltage();
1374+
13711375
while (tankCursor < eligibleCount) {
13721376
DynamicJsonDocument doc(1024);
13731377
doc["client"] = gDeviceUID;
@@ -1376,6 +1380,11 @@ static void sendDailyReport() {
13761380
doc["time"] = reportEpoch;
13771381
doc["part"] = static_cast<uint8_t>(part + 1);
13781382

1383+
// Include VIN voltage in the first part of the daily report
1384+
if (part == 0 && vinVoltage > 0.0f) {
1385+
doc["vinVoltage"] = vinVoltage;
1386+
}
1387+
13791388
JsonArray tanks = doc.createNestedArray("tanks");
13801389
bool addedTank = false;
13811390

@@ -1844,3 +1853,41 @@ static void triggerRemoteRelays(const char *targetClient, uint8_t relayMask, boo
18441853
}
18451854
}
18461855
}
1856+
1857+
// ============================================================================
1858+
// Notecard VIN Voltage Reading
1859+
// ============================================================================
1860+
1861+
static float readNotecardVinVoltage() {
1862+
J *req = notecard.newRequest("card.voltage");
1863+
if (!req) {
1864+
Serial.println(F("Failed to create card.voltage request"));
1865+
return -1.0f;
1866+
}
1867+
1868+
J *rsp = notecard.requestAndResponse(req);
1869+
if (!rsp) {
1870+
Serial.println(F("No response from card.voltage"));
1871+
return -1.0f;
1872+
}
1873+
1874+
// Check for error response
1875+
const char *err = JGetString(rsp, "err");
1876+
if (err && strlen(err) > 0) {
1877+
Serial.print(F("card.voltage error: "));
1878+
Serial.println(err);
1879+
notecard.deleteResponse(rsp);
1880+
return -1.0f;
1881+
}
1882+
1883+
float voltage = (float)JGetNumber(rsp, "value");
1884+
notecard.deleteResponse(rsp);
1885+
1886+
if (voltage > 0.0f) {
1887+
Serial.print(F("Notecard VIN voltage: "));
1888+
Serial.print(voltage);
1889+
Serial.println(F(" V"));
1890+
}
1891+
1892+
return voltage;
1893+
}

TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ struct TankRecord {
182182
double smsAlertTimestamps[10]; // Track last 10 SMS alerts per tank
183183
};
184184

185+
// Per-client metadata (VIN voltage, etc.)
186+
#ifndef MAX_CLIENT_METADATA
187+
#define MAX_CLIENT_METADATA 20
188+
#endif
189+
190+
struct ClientMetadata {
191+
char clientUid[48];
192+
float vinVoltage; // Blues Notecard VIN voltage from daily report
193+
double vinVoltageEpoch; // When the VIN voltage was last updated
194+
};
195+
196+
static ClientMetadata gClientMetadata[MAX_CLIENT_METADATA];
197+
static uint8_t gClientMetadataCount = 0;
198+
185199
static ServerConfig gConfig;
186200
static bool gConfigDirty = false;
187201

@@ -1089,6 +1103,7 @@ static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
10891103
<th>Site</th>
10901104
<th>Tank</th>
10911105
<th>Level</th>
1106+
<th>VIN Voltage</th>
10921107
<th>Status</th>
10931108
<th>Updated</th>
10941109
<th>Refresh</th>
@@ -1202,11 +1217,12 @@ static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
12021217
percent: client.percent,
12031218
alarm: client.alarm,
12041219
alarmType: client.alarmType,
1205-
lastUpdate: client.lastUpdate
1220+
lastUpdate: client.lastUpdate,
1221+
vinVoltage: client.vinVoltage
12061222
});
12071223
return;
12081224
}
1209-
tanks.forEach(tank => {
1225+
tanks.forEach((tank, idx) => {
12101226
rows.push({
12111227
client: client.client,
12121228
site: client.site,
@@ -1216,20 +1232,28 @@ static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
12161232
percent: tank.percent,
12171233
alarm: tank.alarm,
12181234
alarmType: tank.alarmType || client.alarmType,
1219-
lastUpdate: tank.lastUpdate
1235+
lastUpdate: tank.lastUpdate,
1236+
vinVoltage: idx === 0 ? client.vinVoltage : null // Only show VIN on first tank per client
12201237
});
12211238
});
12221239
});
12231240
return rows;
12241241
}
12251242
1243+
function formatVoltage(voltage) {
1244+
if (typeof voltage !== 'number' || !isFinite(voltage) || voltage <= 0) {
1245+
return '--';
1246+
}
1247+
return voltage.toFixed(2) + ' V';
1248+
}
1249+
12261250
function renderTankRows() {
12271251
const tbody = els.tankBody;
12281252
tbody.innerHTML = '';
12291253
const rows = state.tanks;
12301254
if (!rows.length) {
12311255
const tr = document.createElement('tr');
1232-
tr.innerHTML = '<td colspan="7">No telemetry available</td>';
1256+
tr.innerHTML = '<td colspan="8">No telemetry available</td>';
12331257
tbody.appendChild(tr);
12341258
return;
12351259
}
@@ -1241,6 +1265,7 @@ static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
12411265
<td>${row.site || '--'}</td>
12421266
<td>${row.label || 'Tank'} #${row.tank || '?'}</td>
12431267
<td>${formatLevel(row.levelInches)}</td>
1268+
<td>${formatVoltage(row.vinVoltage)}</td>
12441269
<td>${statusBadge(row)}</td>
12451270
<td>${formatEpoch(row.lastUpdate)}</td>
12461271
<td>${refreshButton(row)}</td>`;
@@ -3210,6 +3235,17 @@ static void sendClientDataJson(EthernetClient &client) {
32103235
clientObj["site"] = rec.site;
32113236
clientObj["alarm"] = false;
32123237
clientObj["lastUpdate"] = 0.0;
3238+
3239+
// Add VIN voltage from client metadata if available
3240+
for (uint8_t j = 0; j < gClientMetadataCount; ++j) {
3241+
if (strcmp(gClientMetadata[j].clientUid, rec.clientUid) == 0) {
3242+
if (gClientMetadata[j].vinVoltage > 0.0f) {
3243+
clientObj["vinVoltage"] = gClientMetadata[j].vinVoltage;
3244+
clientObj["vinVoltageEpoch"] = gClientMetadata[j].vinVoltageEpoch;
3245+
}
3246+
break;
3247+
}
3248+
}
32133249
}
32143250

32153251
const char *existingSite = clientObj.containsKey("site") ? clientObj["site"].as<const char *>() : nullptr;
@@ -3610,9 +3646,39 @@ static void handleAlarm(JsonDocument &doc, double epoch) {
36103646
}
36113647

36123648
static void handleDaily(JsonDocument &doc, double epoch) {
3613-
(void)doc;
3614-
(void)epoch;
3615-
// Daily reports are persisted in sendDailyEmail; nothing to do for inbound ack
3649+
// Extract VIN voltage from daily report if present
3650+
const char *clientUid = doc["client"] | "";
3651+
if (clientUid && strlen(clientUid) > 0) {
3652+
// Check if this is part 1 of the daily report (which contains vinVoltage)
3653+
uint8_t part = doc["part"].as<uint8_t>();
3654+
if (part == 1 && doc.containsKey("vinVoltage")) {
3655+
float vinVoltage = doc["vinVoltage"].as<float>();
3656+
if (vinVoltage > 0.0f) {
3657+
// Find or create client metadata entry
3658+
ClientMetadata *meta = nullptr;
3659+
for (uint8_t i = 0; i < gClientMetadataCount; ++i) {
3660+
if (strcmp(gClientMetadata[i].clientUid, clientUid) == 0) {
3661+
meta = &gClientMetadata[i];
3662+
break;
3663+
}
3664+
}
3665+
if (!meta && gClientMetadataCount < MAX_CLIENT_METADATA) {
3666+
meta = &gClientMetadata[gClientMetadataCount++];
3667+
memset(meta, 0, sizeof(ClientMetadata));
3668+
strlcpy(meta->clientUid, clientUid, sizeof(meta->clientUid));
3669+
}
3670+
if (meta) {
3671+
meta->vinVoltage = vinVoltage;
3672+
meta->vinVoltageEpoch = (epoch > 0.0) ? epoch : currentEpoch();
3673+
Serial.print(F("VIN voltage received from "));
3674+
Serial.print(clientUid);
3675+
Serial.print(F(": "));
3676+
Serial.print(vinVoltage);
3677+
Serial.println(F(" V"));
3678+
}
3679+
}
3680+
}
3681+
}
36163682
}
36173683

36183684
static TankRecord *upsertTankRecord(const char *clientUid, uint8_t tankNumber) {
@@ -3804,6 +3870,16 @@ static void publishViewerSummary() {
38043870
obj["alarm"] = gTankRecords[i].alarmActive;
38053871
obj["alarmType"] = gTankRecords[i].alarmType;
38063872
obj["lastUpdate"] = gTankRecords[i].lastUpdateEpoch;
3873+
3874+
// Add VIN voltage from client metadata if available
3875+
for (uint8_t j = 0; j < gClientMetadataCount; ++j) {
3876+
if (strcmp(gClientMetadata[j].clientUid, gTankRecords[i].clientUid) == 0) {
3877+
if (gClientMetadata[j].vinVoltage > 0.0f) {
3878+
obj["vinVoltage"] = gClientMetadata[j].vinVoltage;
3879+
}
3880+
break;
3881+
}
3882+
}
38073883
}
38083884

38093885
String json;

TankAlarm-112025-Viewer-BluesOpta/TankAlarm-112025-Viewer-BluesOpta.ino

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ struct TankRecord {
113113
bool alarmActive;
114114
char alarmType[24];
115115
double lastUpdateEpoch;
116+
float vinVoltage; // Blues Notecard VIN voltage
116117
};
117118

118119
static const size_t TANK_JSON_CAPACITY = JSON_ARRAY_SIZE(MAX_TANK_RECORDS) + (MAX_TANK_RECORDS * JSON_OBJECT_SIZE(10)) + 768;
@@ -238,6 +239,7 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
238239
<th>Site</th>
239240
<th>Tank</th>
240241
<th>Level (ft/in)</th>
242+
<th>VIN Voltage</th>
241243
<th>24hr Change</th>
242244
<th>Updated</th>
243245
</tr>
@@ -372,7 +374,7 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
372374
const rows = state.selected ? state.tanks.filter(t => t.client === state.selected) : state.tanks;
373375
if (!rows.length) {
374376
const tr = document.createElement('tr');
375-
tr.innerHTML = '<td colspan="5">No tank data available</td>';
377+
tr.innerHTML = '<td colspan="6">No tank data available</td>';
376378
tbody.appendChild(tr);
377379
return;
378380
}
@@ -383,12 +385,20 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
383385
<td>${escapeHtml(tank.site, '--')}</td>
384386
<td>${escapeHtml(tank.label || 'Tank')} #${escapeHtml((tank.tank ?? '?'))}</td>
385387
<td>${formatFeetInches(tank.levelInches)}</td>
388+
<td>${formatVoltage(tank.vinVoltage)}</td>
386389
<td>--</td>
387390
<td>${formatEpoch(tank.lastUpdate)}</td>`;
388391
tbody.appendChild(tr);
389392
});
390393
}
391394
395+
function formatVoltage(voltage) {
396+
if (typeof voltage !== 'number' || !isFinite(voltage) || voltage <= 0) {
397+
return '--';
398+
}
399+
return voltage.toFixed(2) + ' V';
400+
}
401+
392402
function statusBadge(tank) {
393403
if (!tank.alarm) {
394404
return '<span class="status-pill ok">Normal</span>';
@@ -815,6 +825,9 @@ static void sendTankJson(EthernetClient &client) {
815825
obj["alarm"] = gTankRecords[i].alarmActive;
816826
obj["alarmType"] = gTankRecords[i].alarmType;
817827
obj["lastUpdate"] = gTankRecords[i].lastUpdateEpoch;
828+
if (gTankRecords[i].vinVoltage > 0.0f) {
829+
obj["vinVoltage"] = gTankRecords[i].vinVoltage;
830+
}
818831
}
819832

820833
String body;
@@ -900,6 +913,7 @@ static void handleViewerSummary(JsonDocument &doc, double epoch) {
900913
rec.alarmActive = item["alarm"].as<bool>();
901914
strlcpy(rec.alarmType, item["alarmType"] | (rec.alarmActive ? "alarm" : "clear"), sizeof(rec.alarmType));
902915
rec.lastUpdateEpoch = item["lastUpdate"].as<double>();
916+
rec.vinVoltage = item["vinVoltage"].as<float>();
903917
}
904918
}
905919

0 commit comments

Comments
 (0)