@@ -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+
185199static ServerConfig gConfig ;
186200static 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
36123648static 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
36183684static 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;
0 commit comments