Logger import
A pressure transducer left in a well records the water level every few minutes for months. Since 2.4, such a record is a logger_series hydrodynamic event: a continuous series, not a pumping test and not a static level (logger_series). Each reading has a datetime (an RFC 3339 instant) and a depth below ground.
@welldot/utils turns a logger's CSV export into those readings. There are no vendor-specific parsers: you map the columns once for your logger model.
From CSV to readings
import { buildLoggerReadings, parseDelimitedText } from '@welldot/utils';
const text = `date;time;level
01/03/2025;00:00;12.40
01/03/2025;01:00;12.38
01/03/2025;02:00;---`;
const { rows } = parseDelimitedText(text); // detects , ; or tab, handles quotes
const { readings, skipped } = buildLoggerReadings(rows, {
headerRows: 1,
dateColumn: 0,
timeColumn: 1,
valueColumn: 2,
valueKind: 'water_column', // the logger reports water above the sensor
sensorDepth: 42, // so depth = 42 − value
dateFormat: 'dmy',
timeZone: 'America/Belem', // or offset: '-03:00'
});
// readings: [
// { datetime: '2025-03-01T00:00:00-03:00', depth: 29.6 },
// { datetime: '2025-03-01T01:00:00-03:00', depth: 29.62 }
// ]
// skipped: [{ row: 3, issue: 'invalid_value' }]
valueKind: 'depth'takes the value as the depth to water;'water_column'converts the height of water above the sensor usingsensorDepth.- Times are local to the logger. Give the IANA
timeZoneof the site, and each reading gets the offset in force at that moment, daylight saving included. A fixedoffsetalso works. - Rows that cannot be read are listed in
skippedwith a reason (invalid_date,invalid_value,missing_sensor_depth,missing_offset), never silently dropped.
Into the file
import { inferLoggerInterval } from '@welldot/utils';
well.hydrodynamic_events = [
...(well.hydrodynamic_events ?? []),
{
id: crypto.randomUUID(),
type: 'logger_series',
datetime: readings[0].datetime,
measurement_method: 'pressure_transducer',
sensor_depth: 42,
interval: inferLoggerInterval(readings), // 60 (minutes)
barometric_compensation: 'barologger', // closed: barologger, vented or none
aggregation: 'none', // closed: none, hourly or daily
readings,
},
];
Check the spec for the meaning of each field; hourly or daily aggregation requires depth_min and depth_max on every reading (logger_series).
Adding a new download
Loggers are downloaded again and again, and the exports overlap:
import { mergeLoggerReadings } from '@welldot/utils';
const { readings: merged, replaced } = mergeLoggerReadings(
event.readings,
newReadings,
'append', // or 'replace'
);
Readings are merged by instant, the incoming value wins on a tie, and the result is sorted. replaced counts the readings that were overwritten (append) or dropped (replace).
Plotting
A year of 15-minute readings is 35,000 points. downsampleLoggerReadings(readings, maxPoints) keeps the first and last readings and the minimum and maximum depth of each bucket, so peaks survive:
import { downsampleLoggerReadings } from '@welldot/utils';
const plotted = downsampleLoggerReadings(event.readings, 2000);
localDateTimeToInstant('2025-03-01T00:00', 'America/Belem') converts one local time to an instant ('2025-03-01T00:00:00-03:00') if you parse files yourself.
Warnings
getOperationWarnings reports logger_reading_below_sensor (a reading deeper than the sensor) and analysis_uses_logger_series (an aquifer analysis citing a logger series, which is not a controlled test) (Operation and production).