Format

Your log, in a format anyone can read.

Everything Odoline exports is documented here: the columns of the CSV and JSON files, the structure of the encrypted backup, and a small script that opens a backup without the app. It is part of the promise.

Export

CSV and JSON export

Export gives one CSV file per table, or one JSON file with all of them. The export format has a version number, odolineExportSchema, currently 4. It is written in the first column of every CSV row, as odoline_schema, and as the odoline_schema key of the JSON file. Version 4 added the electric vehicle columns and the charge location work.

A newer version of the app only ever adds columns at the end of a file. It never renames a column or changes what it means. If the meaning of a column has to change, the version number goes up.

How values are written

  • Choices (for example energy or status) are written as their plain name, without a prefix.
  • Moments in time, the columns ending in _iso, use ISO 8601. In CSV they carry the UTC offset and use your local time when you export; in JSON they are UTC.
  • due_date is a calendar date, yyyy-mm-dd.
  • Lists (fuel_types, applicable_kinds) are joined with |.
  • Yes or no values are true or false.
  • Numbers use a dot as the decimal separator.
  • Distance and quantity columns come with the unit of the same row (distance_unit, quantity_unit); money comes with currency.

In the JSON file, each table is an array named like the CSV file without its extension, next to odoline_schema and exported_at. Each row is an object with the same column names as the CSV, without odoline_schema. Numbers, booleans and empty values keep their JSON type.

Columns

The six export files

Columns are listed in the order they are written. Column names are fixed and in English.

vehicles.csv
#Column
1odoline_schema
2id
3name
4kind
5powertrain
6fuel_types
7distance_unit
8volume_unit
9consumption_unit
10currency
11initial_odometer
12make
13model
14year
15plate
16vin
17status
18sort_order
19created_at_iso
20updated_at_iso
21battery_capacity_kwh
22home_tariff_per_kwh
fillups.csv
#Column
1odoline_schema
2id
3vehicle_id
4vehicle_name
5at_iso
6energy
7fuel_type
8odometer
9trip_distance
10distance_unit
11quantity
12quantity_unit
13unit_price
14total_cost
15currency
16entered_price
17is_full
18missed_previous
19charge_location
20station
21note
22source
23external_key
24created_at_iso
25updated_at_iso
26soc_before
27soc_after
28charge_current
29provider
30session_fee
31charge_duration_min
32charge_power_kw
services.csv
#Column
1odoline_schema
2id
3vehicle_id
4vehicle_name
5at_iso
6odometer
7distance_unit
8service_type_key
9cost
10currency
11vendor
12note
13reminder_id
14source
15external_key
16created_at_iso
17updated_at_iso
expenses.csv
#Column
1odoline_schema
2id
3vehicle_id
4vehicle_name
5at_iso
6odometer
7distance_unit
8category
9amount
10currency
11vendor
12note
13period_months
14source
15external_key
16created_at_iso
17updated_at_iso
reminders.csv
#Column
1odoline_schema
2id
3vehicle_id
4vehicle_name
5title
6service_type_key
7expense_category
8mode
9distance_unit
10interval_distance
11interval_months
12repeat
13due_odometer
14due_date
15notify_before_days
16notify_before_distance
17last_done_at_iso
18last_done_odometer
19status
20snoozed_until_iso
21snoozed_until_odometer
22last_threshold_notified_odometer
23last_date_notified_at_iso
24threshold_notified_at_iso
25external_key
26created_at_iso
27updated_at_iso
custom_service_types.csv
#Column
1odoline_schema
2id
3name
4applicable_kinds
5default_interval_distance
6default_interval_months
7created_at_iso
8updated_at_iso
Backup

The .odoline backup file

The backup is one encrypted file. The structure, version 1, is:

"ODOLBK01"                8 bytes, ASCII
header length             4 bytes, unsigned, big-endian
header                    JSON, UTF-8
ciphertext                AES-256-GCM
tag                       16 bytes, at the very end

The header is not encrypted. It holds the version, the key settings, the nonce, the data schema number and the creation time:

{"v":1,
 "kdf":{"alg":"argon2id","m":19456,"t":2,"p":1,"salt":"<base64, 16 bytes>"},
 "cipher":"aes-256-gcm",
 "nonce":"<base64, 12 bytes>",
 "schema":<database schema number>,
 "createdAt":"<ISO 8601, UTC>"}
  • Key. Argon2id with 19456 KiB of memory, 2 passes and 1 lane, applied to your passphrase as UTF-8 text and the 16-byte salt, gives a 32-byte AES key.
  • Encryption. AES-256-GCM with the 12-byte nonce. The associated data is everything before the ciphertext: the 8 magic bytes, the 4 length bytes and the header. Changing any byte of the header therefore breaks the tag.
  • Inside. The decrypted bytes are a gzip file. Decompressed, it is JSON of the form {"tables": {"<table>": [<row>, …]}, "photos": [{"id", "fileName", "data"}]}. Rows use the database column names, not the export columns above. Each photo is a receipt image, with data in base64 and a fileName made of letters, digits and hyphens plus a short extension.
  • Wrong passphrase or damaged file. When the tag does not match, the first bytes of the decrypted data tell the two apart: the right key gives the start of a gzip file, a wrong key gives noise. A damaged file is never reported as a wrong passphrase.

There is no key stored anywhere. Without the passphrase, nobody can open the file, us included.

The Odoline backup screen: one encrypted file with every vehicle and entry, locked with a passphrase only you know
Reader script

Open a backup without the app

odoline_read.py is a single Python file under the MIT licence. It needs Python 3.9 or newer. It decrypts a backup and writes the tables as JSON, optionally as CSV, and the receipt photos as files. It also needs two packages; pip picks versions that fit your Python:

pip install cryptography argon2-cffi
python3 odoline_read.py my-backup.odoline --out my-log --csv

It asks for the passphrase without showing it. For scripts, the passphrase can be set in the ODOLINE_PASSPHRASE environment variable instead. The script writes backup.json (with schema, createdAt and tables), the photos in photos/ and, with --csv, one CSV file per table. The folder defaults to the file name without its extension (or the name plus -out if there is no extension). It must be new or empty: the script refuses to write into a folder that already has files, and it never writes outside that folder.

Each photo is saved as <id>.jpg, .png, .gif, .webp or .heic, with the type read from the first bytes of the image (JPEG, PNG, GIF, WebP or HEIC). If the type is not one of these, or the id is not a plain name of letters, digits and hyphens, the photo keeps its original file name from the backup.

Exit codes
CodeMeaning
0Done.
2Wrong passphrase.
3The file is damaged or not supported.
4The script could not run: a package is missing, the options are wrong, the output folder already has files, or the output folder cannot be written.

Problems are printed as one line, without a stack trace and without the passphrase.

A log you keep.

Be first to try Odoline. One email on launch day.

One email when Odoline launches. No newsletter. Privacy