Skip to content

Metering Imports

This guide covers importing metering (consumption and production) readings into OpenZEV.

Import Overview

OpenZEV supports two import pathways:

  1. CSV/Excel Files — Column-mapped spreadsheets
  2. SDAT-CH Format — Swiss standard metering data exchange (ebIX XML)

CSV imports use a preview-first validation workflow: you upload, review the column mapping and a preview (no data written yet), then confirm the import. SDAT-CH has no preview step: the file imports directly and an import protocol opens automatically when anything was skipped or failed. Every import creates a protocol accessible from the import history.

Switching community or losing write access resets the import wizard and closes delete and overwrite confirmations. An import already started may still complete; its protocol remains available in that community’s history. A deletion already started can also finish in its original community; it leaves open dialogs in the newly selected community unchanged.

A disabled ZEV is read-only for its managers: CSV preview remains available, but CSV/Excel and SDAT-CH imports are rejected until an administrator re-enables the ZEV. Administrators may still import into a disabled ZEV.

Imports page

CSV/Excel Import

Sample files

The import wizard offers two downloadable samples that match the demo data (CH-DEMO-CONS-0001, CH-DEMO-CONS-0002, CH-DEMO-PROD-0001):

  • standard-readings.csv — one reading per row (meter_id,timestamp,energy_kwh,direction)
  • daily-15min-profile.csv — one day per row with 96 quarter-hour values; set First interval column to 00:00 after selecting it

Legacy .xls files are rejected — save them as .xlsx or CSV first. Maximum file size is 50 MB.

File Format

Prepare your data as CSV or Excel. Two row layouts are supported (select Row format in the wizard):

Standard — one reading per row:

Column Required Format Example
meter_id ✓ Meter ID string CH-DEMO-CONS-0001
timestamp ✓ ISO 8601 or Date/time format below 2026-01-15 14:00:00
energy_kwh ✓ Decimal (kWh, , accepted as .) 1.2500
direction ✗ in, out, or an OBIS code (inferred from meter type when blank) in

Daily profile — one day per row with N interval values:

Column Required Format Example
meter_id ✓ Meter ID string CH-DEMO-CONS-0001
date ✓ Calendar date (see Date/time format) 15.01.2026
direction ✗ in, out, or an OBIS code — applies to the whole row 1-1:1.29.0*255
00:00 … 23:45 ✓ N decimal interval values (default 96 × 15 min) 0.2500

Empty interval cells are skipped. A daily row with no values at all is rejected with Row contains no interval values. — in preview and import alike. Delimiter may be any single character (for example ,, ;, |, or tab), or \t (typed escape for tab).

Step-by-Step Import

  1. Go to Import history (sidebar Metering → tab Import history, or /metering/imports directly). Select the target ZEV with the community name above the page title — CSV preview and import require it (zev_id, API 400 without it) and only meters in that ZEV are imported; other meters are reported as missing.

  2. Upload File

    • Click New Import
    • Select your CSV or Excel file — or several at once (Ctrl/Shift-click in the file dialog). Each file gets its own card with a Remove button
    • Click Next: Configuration

    All selected files are imported with the same settings (column mapping, date format, overwrite mode), so select files that share one layout. Each file becomes its own entry in the import history and protocol. Selecting files again replaces the current selection.

  3. Check the detected settings

    • OpenZEV reads the first file and fills in the settings for you: delimiter, header row, one value per line or one day per row (with its interval and number of values), the meter ID, date/time and direction columns, and the date format. A note above the settings says what was detected.
    • If something could not be detected, the note names it and the field keeps a default — fill it in yourself.
    • Change anything that is wrong; your changes are kept when you go back and forth. Detect again discards them and re-reads the file.
    • With several files, the settings come from the first file and apply to all of them.

    Configure Mapping (only needed where the detection is wrong)

    • Assign columns in your file to OpenZEV fields:
      • Meter ID column → which column?
      • Timestamp/Date column → which column?
      • Energy column or First interval column → which column?
      • Direction column (optional) → which column says consumption vs. feed-in?
    • Set Date/time format as a Python strptime pattern (e.g. %d.%m.%Y, %Y-%m-%dT%H:%M:%S%z), or leave empty for auto-detect. The format must capture the full calendar date (year + month + day); %d.%m is rejected. Day-of-year (%Y-%j) is accepted.
    • Set Timestamps without an offset are to Swiss time (CET/CEST) (the default) or UTC — see Timestamp Handling.
    • For daily profiles set Intervals per row and Minutes per interval.

    Native Excel datetime cells ignore Date/time format — they are read as-is, in the zone chosen above. The format only applies to text cells.

  4. Preview (required for CSV)

    • Click Load Preview. Configuration changes, including toggling overwrite, invalidate the preview — reload before importing. Selecting or removing a file also requires a new preview. With several files, each gets its own preview section; one file with errors blocks the whole import, and a meter missing from any file is listed once.
    • Check Metering point exists per row; missing IDs are listed with copy/download actions and block Start Import until the meters exist.
    • Check Direction: where each row's readings will land — Consumption (in), Feed-in (out), or both when a bidirectional meter's row splits by sign. If a whole file shows the wrong direction, the Direction column mapping is wrong.
    • Check Existing data: the database already holds that reading (standard rows), or the day already holds readings for that meter/direction (daily rows). Enabling overwrite shows an estimate of how many existing readings will be replaced; fully duplicate rows never block the import — they are skipped in skip mode; overwrite mode replaces only changed values. Identical values keep their original source and batch.
    • Review Errors: parse failures and empty daily rows. Values above 99999999.9999 kWh are rejected: they cannot be stored. Partially duplicate daily rows show no error — the existing slots are skipped and missing ones are imported (gap-fill).
  5. Review Import Options

    • Overwrite existing readings for the same metering point + timestamp + direction (unchecked by default). When checked, re-importing replaces changed existing readings that match; when unchecked, existing readings are kept and duplicates are skipped per slot. Overwrites are reported as a protocol Note (Overwrote N existing readings.), not as errors.
    • After changing this option, reload the preview. Fully existing daily rows are skipped by default and replaced with overwrite enabled. Invalid values block import in either mode.
  6. Confirm Import

    • Click Start Import (blocked while the preview is missing, outdated, has errors, or lists missing meters)
    • The system writes data and shows a result notification. Imports with errors or overwrite notes also open Import Protocol; other protocols can be opened from the history table.

Import Protocol

Open Import Protocol from the history table to see totals (Total rows / Imported rows / Skipped rows), an informational Notes section (e.g. overwrite counts), and a Skipped row reasons table (row, meter, reason). Overwrites show a success toast; only true failures show an error toast. Both errors and overwrite notes auto-open the protocol.

For SDAT-CH files the protocol shows the structured outcome instead of row totals: Accepted (billable data written), Held (only non-billable quarantine values written — still a success), No changes (valid replay), Corrections applied (only overwrites, no new rows), or Rejected (nothing written). An observation accounting lists new, overwritten, held, promoted, duplicate, stale, excluded and rejected observations — promoted values are a subset of the created/overwritten rows, not extra energy. Each finding carries its stable cause plus the source meter, block/document, delivery and position needed to resolve it, and the delivery identity (delivery ID, creation, sender/receiver) is shown for traceability.

SDAT-CH import protocol with the structured correction outcome

Duplicate handling: the same metering point + timestamp + direction is stored once. Fully existing daily rows show a non-blocking preview notice, so a file with existing days and newly appended days can be imported together. Re-importing without overwrite skips existing slots and imports missing ones; fully duplicate rows are skipped with a Duplicate reading error.

SDAT-CH Format

OpenZEV imports E66 ValidatedMeteredData files for ZEV/vZEV billing: validated active-energy customer/producer deliveries uploaded manually as XML (.xml) or gzipped XML (.xml.gz, .gz). That is the supported release profile — not all-role SDAT-CH exchange. Automated receipt, acknowledgements/error responses, LEG settlement and other market roles (supplier, balance-group, grid operator, switching) are deferred.

  1. Go to Metering → Import history and click New Import
  2. Choose the SDAT-CH (ebIX XML) format
  3. Upload your SDAT-CH file, or several at once (one import per file)
  4. OpenZEV automatically parses metering point IDs, timestamps (with the offset the file carries) and values (kWh)
  5. There is no preview: the import runs immediately, and the import protocol opens automatically if any rows were skipped or failed. With several files they are imported one after another; if some fail, the others stay imported and only the failed files remain selected so you can retry.

Supported profile

Aspect Supported Excluded / deferred
Message E66 ValidatedMeteredData, schema ValidatedMeteredData_16 E31 aggregates, acknowledgements, error reports, other roots
Business reason E88 billing, C38 information C40 community management, queries, switching, master data
Product / unit Active energy (8716867000030) in kWh Reactive energy, LEG products, other units
Direction Customer/producer consumption (IN) and production (OUT) Grid-interconnection (exchange) points
Resolution 15-minute intervals Hourly and other resolutions
Quality Unflagged and estimated (56) values bill; temporary (21) values are held —
Transport Manual file upload (XML, gzip) Automated exchange, ACK/error messages

Unsupported message types, reasons, products and units reject the file; reactive-energy, LEG and exchange blocks are reported skips. Automated exchange, acknowledgements and other market roles remain outside this manual receiver.

Real DSO production file variants and the full 50 MB exchange volume have not been exercised in production yet.

Complete intervals and atomic rejection

Each billing block must carry every position of its declared interval and fit within the delivery's ReportPeriod. Its IN/OUT direction must match the configured meter type; bidirectional points accept both. An invalid file (unknown or ambiguous meter, incompatible unit, out-of-range or missing position, conflicting values, unknown product) rejects the whole file: no observations are written, existing data is unchanged, and the protocol names the cause with the affected meter, block and position. Reactive-energy, LEG and exchange blocks are intentional skips, reported as notes rather than failures. Separate files in a multi-file upload keep independent outcomes.

Corrections

A replacement delivery (Status 5) with a newer Creation overwrites the billable values it names; an identical repeat is idempotent and changes nothing, while a different value at equal Creation rejects the file and reports both document identities. Overlaps with CSV/manual readings reject the file and preserve the existing data. Corrections that touch already invoiced periods flag those invoices for review instead of changing them — see Invoice Management.

Cancellation deliveries (Status 1) are rejected with a cancellation error in the import protocol, without changing readings or invoices or blocking billing. For corrected readings, request a replacement delivery (Status 5) from your grid operator.

SDAT-CH uploads open the result protocol even for held-only files and unchanged replays. Its counters distinguish billable writes, held values and intentional skips. A multi-file upload opens one protocol; the other files remain available in history.

Recorded reading changes

Old readings are muted and the new readings are bold for easier comparison.

Open an import's Protocol to inspect Reading changes: meter, Swiss interval with UTC offset, direction, change kind, old/new kWh and source document when available. CSV logs have no SDAT delivery identity. A missing previous value differs from zero; promotion means a held value became billable, not that an invoice changed. Before/after metadata was not recorded.

Pages show up to 50 events and the retained count. An incompleteness warning means additional events were not saved; their number is unknown and pagination cannot recover them. Empty historical logs do not prove there were no changes; confirmed unchanged imports and metadata-only histories are labelled separately.

If loading fails, use Retry. Previously loaded history stays visible after a temporary refresh failure, but is hidden if the import is deleted or access is denied. The view is available to admins, managers and viewers.

Held (non-billable) values

Values from information-only deliveries (C38) and temporary values (condition 21) are held in quarantine and excluded from billing, charts and invoices. A file that only holds values is still a successful import (held, not a failure). A validated delivery can write billable data for a held slot when it is new enough, removing the eligible held predecessor. A newer held observation can remain.

You can read the held values themselves:

  • In the import protocol. Remaining held observations lists current rows associated with that import. Its counters describe the original import; later deliveries may replace or promote held rows, so fewer rows—or none—may remain. The accepted counterpart shown is also current, not an import-time snapshot.
  • On the raw readings table. Open Metering → Chart, pick the meter and period, and use Held observations below the raw readings table — see Reading Quality and Held Observations.

Held values never appear in accepted consumption, feed-in or billing totals, and a held value does not replace an accepted reading for the same interval. Held values cannot be managed from the UI: correcting one requires a validated replacement delivery from your grid operator. The list is read-only and available to admins, managers and viewers; participants do not see it.

XML and gzip uploads are limited to 50 MB, with a 100 MB limit after decompression.

Timestamp Handling

OpenZEV stores every reading as an exact moment in time and does all calendar work — days, billing periods, tariff hours, charts — in Swiss time. What matters on import is how a timestamp in your file is read:

  • A timestamp with an offset (2026-01-15T14:00:00+01:00, as in SDAT-CH files, or …Z for UTC) is read exactly as written.
  • A timestamp without an offset (2026-01-15 14:00:00) is read in the zone you choose under Timestamps without an offset are: Swiss time (CET/CEST) by default, which is what Swiss meter and grid-operator exports use. Choose UTC only for a source that really exports UTC without saying so.
  • A daily-profile row covers its Swiss calendar day: its first interval starts at 00:00 Swiss time.

Daylight saving time is handled for Swiss-time files:

  • On the last Sunday of October, 02:00–02:59 happens twice. When a file lists that hour twice for a meter, the first occurrence is read as summer time and the second as winter time — both are imported.
  • On the last Sunday of March, 02:00–02:59 does not exist. Rows at those times with energy 0 are skipped with a warning in the import protocol; a row with energy there is reported as an error.
  • A daily-profile row has 92 quarter-hours on the March day, 96 on ordinary days and 100 on the October day. On the March day, a 96-column row may leave the missing 02:00 hour empty (or 0), or leave the last four columns empty. On the October day the row needs all 100 values: set Intervals per row to 100 for such files (the extra columns stay empty on ordinary days).

The import protocol shows which zone each CSV import used.

The Date/time format field is a Python strptime pattern controlling how text timestamps are parsed; empty means auto-detect (ISO first, then day-first European fallback for dates like 07.01.2026). The pattern must include the full date — %d.%m.%Y, %Y-%m-%dT%H:%M:%S%z, %Y-%j — while %d.%m or %H:%M are rejected.

Pick the zone per file to match what the file contains: a UTC file imported as Swiss time lands one or two hours early, and a Swiss-time file imported as UTC one or two hours late.

Imports from before October 2026

Earlier versions read timestamps without an offset as UTC. If your files were in Swiss time, those readings sit one hour (winter) or two hours (summer) late: the first hours of a month show up on the last day of the previous month, and tariff hours and period boundaries are off by the same amount. The protocol of such an import shows UTC (imported before October 2026).

Your server operator can move these imports to the right time with a management command. It lists the affected imports, shows what would change without writing anything, and only applies when asked:

python manage.py reanchor_readings --list
python manage.py reanchor_readings --batch <batch-id> --batch <batch-id>
python manage.py reanchor_readings --batch <batch-id> --batch <batch-id> --apply

Select all months of a meter together — a month's first hours move onto the previous month's last hours. Each import can be moved only once. If invoices that are already approved, sent or paid cover the moved readings, the command asks for --allow-invoiced; those invoices themselves never change. Regenerate draft invoices afterwards.

Common Import Scenarios

Scenario 1: Hourly Solar + Consumption

File: readings_2026-01.csv

meter_id,timestamp,energy_kwh
CH12345-solar,2026-01-15 00:00:00,0.000
CH12345-solar,2026-01-15 01:00:00,0.000
CH12345-solar,2026-01-15 12:00:00,2.150
CH12346-load,2026-01-15 00:00:00,0.750
CH12346-load,2026-01-15 01:00:00,0.630
  1. Upload file
  2. Map columns (meter_id, timestamp, energy_kwh)
  3. Check the detected timestamp format and the preview (2 meters, one row per reading)
  4. Import

Scenario 2: Utility SDAT-CH Export

Utility provides file: 20260315_metering.xml (or .xml.gz)

  1. Click New Import and choose SDAT-CH (ebIX XML) (select the target ZEV first)
  2. OpenZEV auto-parses metadata
  3. The import runs without a preview; check the protocol for skipped rows

Handling Import Errors

"Metering point not found"

The CSV references a meter ID that doesn't exist in the selected ZEV. Solutions:

  1. Check metering point ID spelling in CSV
  2. Create missing metering points in Metering Points — the preview banner lets you copy or download the missing IDs and links straight there
  3. Re-run import (preview first — Start Import stays blocked while meters are missing)

"Duplicate reading for metering_point + timestamp + direction"

The reading already exists. Without overwrite, fully duplicate rows are skipped; partially duplicate daily rows gap-fill the missing slots. With Overwrite existing readings checked, matching readings with different values are updated and the protocol notes Overwrote N existing readings.

Two files for the same meter, one consumption and one feed-in? Grid operators often deliver a profile export as one file per OBIS code — grid import as 1.29.0, feed-in as 2.29.0 — with the same metering point ID and positive values in both. Imported without a Direction column, the second file looks like a duplicate of the first and every row is skipped.

Map the OBIS column as the Direction column (in these exports it is the second column, index 1) and both files import onto the same metering point, one as consumption and one as feed-in. The preview's Direction column confirms it before you commit. Note that the field is empty by default — nothing is pre-filled for you.

Do not use Overwrite existing readings to force the second file through: it replaces the first file's readings with the second file's values under the first file's direction, which silently destroys the data you already imported.

"Row contains no interval values."

A daily-profile row carried no interval values at all. Fill in at least one slot — preview and import both reject the empty row.

"Value parse error"

Reading value is not numeric (e.g., "N/A").

Solutions:

  1. Clean CSV: Replace non-numeric error values with 0.000 (or leave the cell empty to skip the slot in daily profiles)
  2. Correct the invalid rows and re-run the import (skipped/failed rows are listed in the import protocol)
  3. Re-run import

Re-importing (Corrections & Updates)

If you find errors in imported data:

  1. Prepare corrected CSV
  2. Go to Metering → Import history
  3. Tick Overwrite existing readings only to replace matching readings (same metering point + timestamp + direction). Unticked (default, safe): new readings are added, existing ones kept, duplicates skipped per slot.
  4. Upload and preview
  5. Confirm

Best practice: Leave overwrite off unless you mean to change history. To remove bad imports entirely, delete the import log (single or period bulk-delete) — period deletion uses the Swiss calendar days on which the imports were created, the same days the history shows.

Deleting imports

Single and bulk deletion remove eligible import logs plus all readings still linked to their batches, including held SDAT-CH values, then refresh metering data. Imports that replaced billable or held values cannot be deleted, because their previous values cannot be restored. Protected imports have no delete action; their protocol explains why. If a bulk selection includes a protected import, the entire deletion is rejected and nothing is removed. The bulk dialog lists the blocking protected imports (filename and creation time) and disables confirmation while any is included. To correct an overwrite import, upload corrected values with overwrite enabled; for SDAT-CH, request a newer correction delivery from your provider. Managers cannot delete imports from a disabled community. Period bulk-delete matches imports created from 00:00 Swiss time on the first day up to midnight after the last day — the visible-count preview counts exactly the same imports. Deleting billable readings flags the affected invoices for review instead of changing them — see Invoice Management.

Data Quality Checks

After import, review Metering → Data Quality (/metering/quality) to see:

  • Coverage per metering point
  • Missing readings
  • Gaps vs. assignment/participant validity windows
  • Unassigned readings / assignment overlap warnings

See Metering Analysis for details.

Generating Sample Data (CLI)

For development, testing, or demo purposes, OpenZEV includes a management command to generate realistic metering data with proper daily patterns.

Usage

python manage.py generate_metering_data <meter_id> <type> --start <date> --days <n> [--interval <interval>]
Argument Required Description
meter_id ✓ Meter ID (e.g. CH1234567890120000000006666665030) or metering point UUID
type ✓ consumption, production, or bidirectional
--start ✓ Start date in YYYY-MM-DD format
--days ✓ Number of days to generate
--interval 15min (default) or hourly
--peak-kwh Override auto-scaled peak kWh per interval
--net-metering Simulate grid connection meter: production offsets consumption. Auto-enabled for bidirectional type
--dry-run Show statistics without writing to the database

Examples

# 30 days of consumption at 15-minute intervals
python manage.py generate_metering_data CH1234567890120000000006666665030 consumption \
    --start 2026-01-01 --days 30 --interval 15min

# 90 days of solar production at hourly intervals
python manage.py generate_metering_data CH9876543210987000000000044440859 production \
    --start 2026-01-01 --days 90 --interval hourly

# 60 days of bidirectional data (net metering auto-enabled)
python manage.py generate_metering_data CH1234567890120000000006666665030 bidirectional \
    --start 2026-01-01 --days 60

# Consumption meter behind a solar system (grid connection meter)
python manage.py generate_metering_data CH1234567890120000000006666665030 consumption \
    --start 2026-01-01 --days 30 --net-metering

# Preview without writing
python manage.py generate_metering_data CH1234567890120000000006666665030 consumption \
    --start 2026-01-01 --days 7 --dry-run

When running in Docker:

docker compose exec backend python manage.py generate_metering_data \
    CH1234567890120000000006666665030 consumption --start 2026-01-01 --days 30

Data Profiles

The generated data uses realistic patterns:

  • Consumption: Household load profile with morning and evening peaks, low overnight usage, and weekday/weekend variation.
  • Production: Solar generation bell curve peaking around 13:00, with seasonal variation (higher in summer) and random cloud effects.
  • Bidirectional: Generates both consumption and production readings using net metering (auto-enabled).
  • Net metering (--net-metering): Simulates a grid connection meter where local solar production offsets consumption. Only the surplus is recorded — IN readings reflect grid import (consumption exceeding production), OUT readings reflect grid export (production exceeding consumption).

Duplicate readings (same metering point, timestamp, and direction) are skipped automatically.

Next Steps

Protected correction paths

Individual readings and importer provenance are read-only through the API and Django admin. Correct quantities through an explicit CSV overwrite or a newer SDAT replacement delivery. A newer original delivery cannot replace existing values; the protocol reports those skipped originals separately from replays. The SDAT protocol shows translated findings and relevant identifiers, values and XML error locations when recorded. Older logs may contain fewer details. Original diagnostic text stays in the API log; it is not appended in English to the translated message. Unknown finding codes have a translated fallback with the code. The filename and result come first; an accepted correction is a successful import. Reading changes appear before the reading summary, technical identifiers and findings.

Cancellation deliveries remain unsupported. For an exceptional removal, an admin can delete a point's readings for a date range using Delete data; this is a deletion, not an undo of a replacement, and affected invoices still require review. Request corrected source data before reimporting.

The supported E66 profile requires an explicit 15-minute resolution and complete interval positions. Other schema-valid E66 volumes without a resolution, hourly series and tariff-band observations are not supported. Held observations remain non-billable staging evidence: inspect the uploaded source or community archive; a validated delivery can promote them. There is no manual release action.

Direct reading API create/update/delete operations return 405; corrections use imports or the protected deletion workflow. PostgreSQL is required for production metering correction and identity concurrency guarantees.

CSV overwrite skips identical values and preserves their source evidence and import ownership. Changing an SDAT value deliberately makes it a CSV correction: it clears the SDAT provenance and later SDAT deliveries cannot replace that slot. The confirmation explains this before import. CSV meter lookup uses primary IDs; secondary SDAT aliases are not CSV identifiers.

The completion message distinguishes new readings, corrections, held values and unchanged files. Batch messages count these separately. Reading changes and deletions refresh invoice-review and billing-readiness state. Delete data removes billable and held observations in the chosen range and reports both counts.

Production deployments require PostgreSQL. SQLite is supported for development and tests; MariaDB does not support these billing/identity lock workflows. Historical assignment edits remain a separate management operation: the reading correction flags described here track imports, protected deletion and timestamp repair, not every change to assignment windows. Review affected billing periods when changing historical assignments.