# Logger import

> Turn a pressure transducer or data logger export into logger_series readings, with column mapping, time zones, merging and downsampling, using @welldot/utils.

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`](/format/spec/v2/object-schemas#logger_series-since-v24)). 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

```ts
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 using `sensorDepth`.
- Times are local to the logger. Give the IANA `timeZone` of the site, and each reading gets the offset in force at that moment, daylight saving included. A fixed `offset` also works.
- Rows that cannot be read are listed in `skipped` with a reason (`invalid_date`, `invalid_value`, `missing_sensor_depth`, `missing_offset`), never silently dropped.

## Into the file

```ts
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`](/format/spec/v2/object-schemas#logger_series-since-v24)).

## Adding a new download

Loggers are downloaded again and again, and the exports overlap:

```ts
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:

```ts
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](/utils/guides/operation)).
