# Dateiformate von Tempisto

Tempisto verwendet zwei JSON-Dokumente:

- Session (`.session.json`) — ein Training.
- Plan (`.plan.json`) — ein Zeitplan aus Sessions.

Alle Dauern sind ISO-8601-Dauerstrings der Form `P[nD][T[nH][nM][nS]]`, zum
Beispiel `PT15M`, `PT2M30S`, `PT10S`. Jahre und Monate sind nicht erlaubt. Eine
Dauer wird immer als String geschrieben, nie als Sekundenzahl.

## Session-Datei (`.session.json`)

Objekt auf oberster Ebene:

- `format` (String, Pflicht): muss `tempisto.session/1` sein.
- `name` (String, Pflicht): Anzeigename der Session.
- `sport` (String, optional): Standard `running`.
- `notes` (String, optional): freie Notizen.
- `steps` (Array, Pflicht): Liste aus Step-Objekten oder Repeat-Bloecken.
  Mindestens ein Step.
- `announcements` (Objekt, optional): Sprach-Einstellungen und Ansageregeln.
- `modifiedEpochMillis` (Zahl, optional): Zeitpunkt der letzten Speicherung in
  Epoch-Millisekunden. Wird von den Editoren geschrieben; handgeschriebene
  Dateien duerfen sie weglassen.
- `archived` (Bool, optional): archivierte Sessions bleiben auf der Platte und
  bleiben von Plaenen referenziert, sie verschwinden nur aus den Auswahllisten.

### Step-Objekt

- `type` (String, Pflicht): eines von `warmup`, `work`, `recovery`, `cooldown`,
  `rest`.
- `name` (String, optional): eigener Name des Steps.
- `duration` (Objekt, Pflicht): genau eine der folgenden Formen.
  - `{ "time": "<ISO-Dauer>" }` — eine feste Zeit.
  - `{ "distance": { "value": <Zahl>, "unit": "m" | "km" | "mi" } }` — eine
    feste Distanz.
  - `{ "until": { "metric": <Metrik>, "below" | "above": <Zahl>, "for":
"<ISO-Dauer>" }, "maxTime" | "maxDistance": ... }` — endet, wenn eine
    Bedingung erfuellt ist, begrenzt durch `maxTime` (ISO-Dauer) oder
    `maxDistance` (Distanz-Objekt). Die Begrenzung ist Pflicht.
    - Metriken fuer `until`: `power`, `pace`, `speed`, `heart_rate`, `cadence`.
    - Genau eines von `below` oder `above` verwenden. `for` ist optional und
      gibt an, wie lange die Bedingung gelten muss.
  - `{ "lap": true }` — endet, wenn die Lap-Taste gedrueckt wird.
- `target` (Objekt, optional): eine Metrik als Schluessel, zum Beispiel
  `{ "power": { "relative": [0.65, 0.75] } }`. Metriken: `power`, `pace`,
  `speed`, `heart_rate`, `cadence`.
- `announce` (Objekt, optional): `{ "rules": [ ... ] }` mit derselben
  Regelform wie `announcements.rules`.

### Repeat-Block

- `{ "repeat": <Zahl>, "steps": [ ... ] }`
- `repeat` ist die Anzahl der Wiederholungen (1 oder mehr).
- Bloecke sind verschachtelbar: Die `steps` eines Repeat-Blocks duerfen
  Step-Objekte oder weitere Repeat-Bloecke enthalten.

### Announcements-Objekt

- `voice` (Objekt):
  - `enabled` (Bool)
  - `rate` (Zahl, 0.35 bis 0.75), wobei 0.5 der Systemstandard ist
  - `duckOtherAudio` (Bool)
  - `voiceIdentifier` (String, optional)
  - `languageCode` (String, optional), ein BCP-47-Tag
- `haptics` (Bool)
- `rules` (Array): jede Regel traegt
  - einen Trigger, genau einen von
    - `{ "every": { "distance": { "value": <Zahl>, "unit": "m" | "km" | "mi"
} } }`
    - `{ "every": { "time": "<ISO-Dauer>" } }`
    - `{ "on": "step_start" | "step_end" | "target_violation" }`
    - `{ "on": "last_seconds", "at": "<ISO-Dauer>" }`
  - `say` (Array aus Strings, Pflicht): die Ansage-Felder. Die Liste darf nicht
    leer sein.
  - `minInterval` (ISO-Dauer, optional): Mindestabstand zwischen zwei
    Ausloesungen der Regel.

### Ansage-Felder (`say`)

Zusammengesetzte Felder folgen der Grammatik `[<scope>_][<aggregate>_]<metric>`:

- scope: `total`, `step`, `split`, `lap`
- aggregate: `avg`, `max`, `min`, `remaining`
- metric: `time`, `distance`, `pace`, `speed`, `power`, `cadence`,
  `heart_rate`, `elevation`

Standalone-Felder: `step_name`, `step_target`, `target_deviation`, `countdown`,
`distance_source`, `step_duration`, `next_step`, `go`, `average_speed`.

Beispiele gueltiger Felder: `step_name`, `step_target`, `countdown`,
`step_avg_power`, `step_time`, `split_avg_pace`, `total_distance`,
`total_avg_power`, `total_time`, `target_deviation`.

## Plan-Datei (`.plan.json`)

Objekt auf oberster Ebene:

- `format` (String, Pflicht): muss `tempisto.plan/1` sein.
- `name` (String, Pflicht).
- `starts` (String, Pflicht): Startdatum des Plans als `YYYY-MM-DD`.
- `entries` (Array, Pflicht): jeder Eintrag traegt
  - `session` (String, Pflicht): Dateiname einer Session-Datei.
  - `rrule` (String) oder `date` (String, `YYYY-MM-DD`) — genau eines von
    beiden.
  - `rrule` ist eine iCal-Wiederholungsregel, zum Beispiel
    `FREQ=WEEKLY;BYDAY=MO,WE;COUNT=16`.

## Beispiele

`5x1000.session.json`:

```json
{
  "format": "tempisto.session/1",
  "name": "5 × 1000 m @ CP",
  "sport": "running",
  "notes": "Keep the recoveries honest.",
  "steps": [
    {
      "type": "warmup",
      "duration": { "time": "PT15M" },
      "target": { "power": { "relative": [0.65, 0.75] } }
    },
    {
      "repeat": 5,
      "steps": [
        {
          "type": "work",
          "name": "1000 m",
          "duration": { "distance": { "value": 1000, "unit": "m" } },
          "target": { "power": { "relative": [0.98, 1.02] } },
          "announce": {
            "rules": [
              { "on": "step_start", "say": ["step_name", "step_target"] },
              { "on": "last_seconds", "at": "PT10S", "say": ["countdown"] },
              { "on": "step_end", "say": ["step_avg_power", "step_time"] }
            ]
          }
        },
        {
          "type": "recovery",
          "duration": {
            "until": { "metric": "heart_rate", "below": 130, "for": "PT10S" },
            "maxTime": "PT3M"
          }
        }
      ]
    },
    { "type": "cooldown", "duration": { "time": "PT10M" } }
  ],
  "announcements": {
    "voice": { "enabled": true, "rate": 0.5, "duckOtherAudio": true },
    "haptics": true,
    "rules": [
      {
        "every": { "distance": { "value": 500, "unit": "m" } },
        "say": ["split_avg_pace", "total_distance"]
      },
      { "every": { "time": "PT5M" }, "say": ["total_avg_power", "total_time"] },
      {
        "on": "target_violation",
        "say": ["target_deviation"],
        "minInterval": "PT20S"
      }
    ]
  }
}
```

`base-build.plan.json`:

```json
{
  "format": "tempisto.plan/1",
  "name": "Base build, 8 weeks",
  "starts": "2026-08-10",
  "entries": [
    {
      "session": "easy-45.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=16"
    },
    {
      "session": "5x1000.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=TU;COUNT=8"
    },
    {
      "session": "long-run.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=SU;COUNT=8"
    },
    { "session": "benchmark.session.json", "date": "2026-10-04" }
  ]
}
```

## Regeln

- Dauern sind Strings in ISO-8601-Form; Jahre und Monate sind nicht erlaubt.
- Ein Plan-Eintrag hat `rrule` oder `date`, nie beides und nie keines von
  beiden.
- Eine Regel, die nichts sagt, hat keine Wirkung: `say` darf nicht leer sein.
- `target_deviation` liest sich nur innerhalb einer `target_violation`-Regel
  korrekt.
- Ein zeit- oder distanzgebundener Step muss groesser als null sein.
- Ein `until`-Step ohne `maxTime` oder `maxDistance` ist nicht gueltig.
- Ein `repeat`-Wert muss mindestens 1 sein.
