Skip to content

Mapping — Concept2

Source: a Concept2 Logbook (log.concept2.com) season CSV export — one row per workout, covering RowErg, SkiErg, and BikeErg. Pillars: A + B. Mapper: mapConcept2mapConcept2(csv) → { records, warnings }, one Session per row plus an avg-HR Measurement per workout.

Structural correspondence

Concept2 CSVOpenBody
Log IDclientRecordId; DescriptionSession.name; Commentsnotes
Type (RowErg/SkiErg/BikeErg)disciplines (rowing / concept2:skierg / cycling) and exerciseRef (row.erg / ski.erg / opaque-only — see notes)
a fixed piece ("2000m row", "30:00 ski")a distance- or time-scored WorkUnit; the piece’s other metric rides in extension.concept2 (§5.5 — a non-continuous unit can’t carry a metric that contradicts its scoring)
a fixed-interval workout ("8x500m/0:30r row")a Block of per-interval WorkUnits, each with performance.rest — the structure is recovered from the PM5-generated Description
a “just row” (arbitrary end)a continuous WorkUnit carrying time + distance + energy together
Stroke Rate/Cadence / Avg Wattsperformance.intensity ({ dimension: "cadence" } / { dimension: "power" }) on single-piece workouts (§5.13)
Avg Heart Ratea Pillar A heart_rate_mean Measurement spanning the workout, linked via measuredBy (§7.2)
pace, stroke count, drag factor, cal/hr, …extension.concept2 (canonical-plus-residue)

Input (excerpt of examples/concept2/concept2-season-sample.csv)

"Log ID",Date,Description,"Work Time (Formatted)","Work Time (Seconds)",...,"Work Distance",...,"Stroke Rate/Cadence",...,"Avg Watts",...,"Avg Heart Rate","Drag Factor",...,Type,...,Comments,...
90000001,"2026-03-02 06:45:00","2000m row",7:45.3,465.3,,,2000,,28,217,1:56.3,221,1063,137,172,118,40,Hwt,RowErg,Yes,"2k test — season best attempt",...
90000002,"2026-03-04 07:10:00","8x500m/0:30r row",15:20.0,920.0,4:00.0,240,4000,184,30,460,1:55.0,228,1085,185,158,116,40,Hwt,RowErg,No,,...

Output (the 2k test — a fixed-distance piece)

{
"id": "c2-90000001",
"recordType": "Session",
"subject": "subj-001",
"clientRecordId": "90000001",
"name": "2000m row",
"notes": "2k test — season best attempt",
"disciplines": ["rowing"],
"intent": "train",
"startTime": "2026-03-02T06:45:00Z",
"endTime": "2026-03-02T06:52:45Z",
"provenance": { "method": "sensor", "sourceApp": "concept2", "device": { "manufacturer": "concept2", "model": "RowErg" } },
"workUnits": [
{
"id": "c2-90000001-wu",
"recordType": "WorkUnit",
"exerciseRef": { "id": "row.erg", "opaque": "RowErg" },
"scoring": "distance",
"performance": {
"distance": { "absolute": { "value": 2000, "unit": "m" } },
"intensity": [
{ "dimension": "cadence", "unit": "/min", "value": { "absolute": { "value": 28 } } },
{ "dimension": "power", "unit": "W", "value": { "absolute": { "value": 221 } } }
]
}
}
],
"links": [{ "type": "measuredBy", "ref": "c2-90000001-hr" }],
"extension": { "concept2": { "pace": "1:56.3", "strokeCount": 217, "calHour": 1063, "dragFactor": 118, "workTimeSeconds": 465.3, "totalCal": 137 } }
}

The interval row ("8x500m/0:30r row") expands to a Block of eight WorkUnits:

{
"id": "c2-90000002-blk",
"recordType": "Block",
"name": "8x500m/0:30r row",
"children": [
{
"id": "c2-90000002-int1",
"recordType": "WorkUnit",
"exerciseRef": { "id": "row.erg", "opaque": "RowErg" },
"scoring": "distance",
"performance": {
"distance": { "absolute": { "value": 500, "unit": "m" } },
"rest": { "absolute": { "value": 30, "unit": "s" } }
}
}
]
}

Notes

  • Machine identity: RowErg ⇒ registry row.erg, SkiErg ⇒ ski.erg (both verified in the registry); BikeErg has no canonical registry id (the registry’s air-bike is a fan bike) so it stays opaque-only. The raw Type string always rides in exerciseRef.opaque.
  • SkiErg’s discipline is concept2:skierg — there is no canonical skierg discipline token, and skiing would misstate an indoor erg (§5.9 namespaced fallback).
  • For interval workouts the CSV’s stroke rate/watts are whole-workout averages — asserting them on any single interval would fabricate per-interval data, so they land in extension.concept2 instead.
  • Timestamps are wall-clock with no offset — pass utcOffset to stamp them (default "Z").