# API-Beschreibung für Telemetrie-Abruf (GET)

**Version:** 1.5  
**Stand:** Juni 2026  
**Herausgeber:** Dexa Solutions GmbH  
**Produkt:** Safe Fire House (SFH)  

---

## 1. Übersicht

Dieses Dokument beschreibt den REST-API-Endpoint, über den ein externes System die aktuellen Telemetriedaten der Safe Fire House Brandwarnanlage **aktiv abruft** (Pull). Die Zentrale ist hier der **Server**, das abrufende System der **Client**. Die Antwort enthält den Datenbaum aus Wache, Fahrzeugen und Rauchsensoren.

| Parameter         | Wert                                                         |
| ----------------- | ------------------------------------------------------------ |
| **Method**        | `GET`                                                        |
| **Pfad**          | `/api/health/telemetry` (mit oder ohne abschließenden `/`)   |
| **Accept**        | `application/json`                                           |
| **Response-Type** | `application/json; charset=utf-8`                            |
| **Frequenz**      | On-Demand (Client-gesteuert)                                 |
| **Rate Limit**    | 5 Requests/Sekunde pro Client-IP, Burst 10 (sonst `429`)     |
| **Caching**       | Antwort bis zu 20 s serverseitig gecacht (siehe Abschnitt 9) |

### 1.1 Authentifizierung

Der Endpoint erfordert ein **Bearer-Token** im `Authorization`-Header. Das Token wird je Zentrale vergeben und vertraulich an das abrufende System übergeben.

| Methode          | Header / Mechanismus    | Beispiel                                     |
| ---------------- | ----------------------- | -------------------------------------------- |
| **Bearer Token** | `Authorization: Bearer` | `Authorization: Bearer bffdc50d5a1173159...` |

Fehlt der Header oder ist das Token ungültig, antwortet der Endpoint mit `401`. Ist serverseitig kein Token konfiguriert, antwortet er mit `503` (fail-closed, keine Datenausgabe).

---

## 2. Endpoint

```
GET https://<zentrale-host>/api/health/telemetry
```

- `<zentrale-host>` = IP oder Hostname der Zentrale im lokalen Netz (z. B. `192.168.1.16`).
- Erreichbar über den Reverse-Proxy der Zentrale; der `Authorization`-Header wird unverändert durchgereicht.
- Der abschließende Slash ist optional (`/api/health/telemetry` und `/api/health/telemetry/` sind gleichwertig).

---

## 3. Request

Es wird **kein** Request-Body gesendet. Erforderlicher Header:

| Header          | Pflicht | Wert             |
| --------------- | ------- | ---------------- |
| `Authorization` | ja      | `Bearer <token>` |

**Beispiel:**

```bash
curl --location 'https://192.168.1.16/api/health/telemetry/' \
  --header 'Authorization: Bearer <token>'
```

---

## 4. Payload-Struktur (Response)

```
Root
├── timestamp
├── fireStation
├── deviceId
└── objects[]
    ├── type            (vehicle | room | hall)
    ├── vehicleId
    ├── sign
    ├── callSign
    ├── vehicleType
    └── smokeDetectors[]
        ├── name
        ├── address
        ├── type
        └── ...
```

> **Hinweis zu Wertetypen:** Alle Schlüssel sind **camelCase**. Alle skalaren Werte werden als **JSON-String** ausgegeben (auch Zahlen und Flags, z. B. `"rssiDevice": "-71"`, `"battery": "false"`, `"alarmState": "0"`). `objects` und `smokeDetectors` sind echte JSON-Arrays.

---

## 5. Root-Objekt

| Key           | Description                                  | Type     | Constraints                                    |
| ------------- | -------------------------------------------- | -------- | ---------------------------------------------- |
| `timestamp`   | Zeitstempel der Erstellung                   | `string` | ISO 8601 UTC (`YYYY-MM-DDTHH:mm:ssZ`)          |
| `fireStation` | Wache (Name, Adresse)                        | `string` | Max. 150 Zeichen                               |
| `deviceId`    | Seriennummer der Zentrale                    | `string` | 14 Zeichen, hexadezimal                        |
| `objects`     | Auflistung der Objekte (Fahrzeug/Raum/Halle) | `array`  | Array von Objekt-Einträgen (siehe Abschnitt 6) |

**Beispiel:**

```json
{
  "timestamp": "2026-06-09T11:24:13Z",
  "fireStation": "Feuerwehr Feuerstadt, Hauptstr. 112, 01234 Feuerstadt",
  "deviceId": "001A2B3C4D5E6F",
  "objects": [ ... ]
}
```

---

## 6. Objekt-Eintrag

Ein Objekt-Eintrag bündelt die Rauchsensoren eines Trägers. Das Feld `type` unterscheidet die Träger-Art. Die fahrzeugspezifischen Felder (`vehicleId`, `sign`, `callSign`, `vehicleType`) sind bei `type = "vehicle"` befüllt; für `room`/`hall` können sie leer bzw. `"n.a."` sein.

| Key              | Description                          | Type     | Constraints                                                  |
| ---------------- | ------------------------------------ | -------- | ------------------------------------------------------------ |
| `type`           | Art des Trägers                      | `string` | Enum: `"vehicle"` \| `"room"` \| `"hall"` (derzeit nur `"vehicle"` belegt) |
| `vehicleId`      | Fahrzeug-Identifikationsnummer (VIN) | `string` | 17 Zeichen; `"n.a."` falls nicht hinterlegt (siehe 8.1)      |
| `sign`           | Kennzeichen                          | `string` | Max. 10 Zeichen; `"n.a."` falls nicht hinterlegt (siehe 8.1) |
| `callSign`       | Funkrufname                          | `string` | Max. 50 Zeichen                                              |
| `vehicleType`    | Fahrzeugtyp                          | `string` | Max. 50 Zeichen; `"n.a."` falls nicht hinterlegt (siehe 8.1) |
| `smokeDetectors` | Auflistung der Rauchsensoren         | `array`  | Array von SmokeDetector-Objekten; `[]` falls keine Melder zugeordnet (siehe 8.2) |

**Beispiel:**

```json
{
  "type": "vehicle",
  "vehicleId": "WVWZZZ3CZWE123456",
  "sign": "FS-FW 112",
  "callSign": "1-HLF20-1",
  "vehicleType": "HLF20",
  "smokeDetectors": [ ... ]
}
```

---

## 7. SmokeDetector-Objekt

Alle Werte sind Strings (siehe Hinweis in Abschnitt 4). Fehlt ein einzelner Datapoint, wird ein typ-konformer Default geliefert (nie `null`) — siehe Abschnitt 8.3.

| Key                 | Description                           | Type     | Constraints                                  |
| ------------------- | ------------------------------------- | -------- | -------------------------------------------- |
| `name`              | Rauchsensorbezeichnung                | `string` | Max. 30 Zeichen                              |
| `address`           | Rauchsensoradresse                    | `string` | 14 Zeichen, hexadezimal                      |
| `type`              | Rauchsensortyp                        | `string` | Konstant `"SFHSS02"`                         |
| `version`           | Hardware-Version                      | `string` | numerisch, ≥ 1                               |
| `group`             | Gruppierung                           | `string` | `0`–`9` oder leer                            |
| `teams`             | Reserviert                            | `string` | i. d. R. leer                                |
| `firmware`          | Firmware-Version                      | `string` | Max. 9 Zeichen, Pattern `[0-9.]+`            |
| `rssiDevice`        | Funkempfangswert Gerät (dBm)          | `string` | numerisch, −128 bis 128                      |
| `rssiPeer`          | Funkempfangswert Sender (dBm)         | `string` | numerisch, −128 bis 128                      |
| `battery`           | Flag: Batterieleistung niedrig        | `string` | `"true"` / `"false"`                         |
| `unreachState`      | Flag: Gerät nicht erreichbar          | `string` | `"true"` / `"false"`                         |
| `unreachCumulative` | Kumulierte Nichterreichbarkeit (Tage) | `string` | numerisch (0–9999) oder `"n.a."` (siehe 8.2) |
| `operationTime`     | Betriebszeit (Tage)                   | `string` | numerisch, 0–9999                            |
| `dirtLevel`         | Verschmutzungsgrad                    | `string` | float-String (z. B. `"0.000000"`)            |
| `smokeLevel`        | Raucherkennungsgrad                   | `string` | float-String (z. B. `"0.000000"`)            |
| `alarmState`        | Alarmstatus                           | `string` | `"0"`–`"3"` (siehe Enum, Abschnitt 7.1)      |
| `voltage`           | Batteriespannung (V)                  | `string` | float-String (0.0–3.2)                       |
| `chamber`           | Flag: Rauchkammer verschmutzt         | `string` | `"true"` / `"false"`                         |
| `errorCode`         | Fehlercode                            | `string` | numerisch, 0–99                              |

**Beispiel:**

```json
{
  "name": "1-HLF20-1 RM1",
  "address": "00AABBCCDDEE11",
  "type": "SFHSS02",
  "version": "1",
  "group": "",
  "teams": "",
  "firmware": "1.0.6",
  "rssiDevice": "-65",
  "rssiPeer": "0",
  "battery": "false",
  "unreachState": "false",
  "unreachCumulative": "0",
  "operationTime": "180",
  "dirtLevel": "0.000000",
  "smokeLevel": "0.000000",
  "alarmState": "0",
  "voltage": "3.000000",
  "chamber": "false",
  "errorCode": "0"
}
```

### 7.1 Enum: alarmState

| Wert  | Bedeutung                                                    |
| ----- | ------------------------------------------------------------ |
| `"0"` | Ruhezustand – Kein Rauch erkannt                             |
| `"1"` | Lokaler Alarm – Rauch erkannt                                |
| `"2"` | Reserviert                                                   |
| `"3"` | Broadcast Alarm – Anderer Sensor in Funkreichweite hat Rauch erkannt |

### 7.2 Flag-Logik

| Flag           | Bedeutung wenn "true"  | Zusatzinfo                       |
| -------------- | ------------------------ | -------------------------------- |
| `chamber`      | Rauchkammer verschmutzt  | Siehe `dirtLevel`                |
| `battery`      | Batterieleistung niedrig | Siehe `voltage` (V)              |
| `unreachState` | Gerät nicht erreichbar   | Siehe `unreachCumulative` (Tage) |

---

## 8. Sonderfälle & Defaults

### 8.1 Fahrzeug-Metadaten nicht deklariert

`vehicleId`, `sign` und `vehicleType` werden je `callSign` aus der Fahrzeug-Stammdatenpflege der Zentrale gelesen. Verhalten pro Feld (einzeln):

| Situation                                    | Ausgabe              |
| -------------------------------------------- | -------------------- |
| `callSign` **fehlt** in der Stammdatenpflege | `"n.a."`             |
| Eintrag vorhanden, **Wert leer**             | `""` (leerer String) |
| Eintrag + Wert vorhanden                     | der Wert             |

`callSign` selbst stammt aus der Fahrzeugliste und ist immer gesetzt.

### 8.2 Gerät nicht erreichbar / nicht gepairt

| Fall                             | Verhalten                                                    |
| -------------------------------- | ------------------------------------------------------------ |
| Melder **gepairt, aber offline** | erscheint im Baum; `unreachState` = `"true"`; `unreachCumulative` = Tage seit letztem Kontakt bzw. `"n.a."`; übrige Werte = **zuletzt bekannter Stand** (kein Live-Funk-Poll beim Abruf) |
| Melder **nicht (mehr) gepairt**  | Melder fehlt im Array. Ein Fahrzeug ohne zugeordnete Melder liefert `"smokeDetectors": []` |

> `unreachCumulative` = `"n.a."` bedeutet „nicht in der Erreichbarkeits-Historie der Zentrale geführt", **nicht** zwingend „erreichbar".

### 8.3 Fehlender Datapoint → typ-konformer Default

Existiert ein einzelner Sensor-Datapoint nicht (abweichendes Geräteprofil o. Ä.), wird statt `null` ein Default ausgegeben:

| Feld(er)                                                     | Default      |
| ------------------------------------------------------------ | ------------ |
| `battery`, `unreachState`, `chamber`                         | `"false"`    |
| `rssiDevice`, `rssiPeer`, `errorCode`, `operationTime`, `alarmState` | `"0"`        |
| `voltage`, `smokeLevel`, `dirtLevel`                         | `"0.000000"` |
| `firmware`, `group`, `version`, `teams` (keine Geräte-Metadaten) | `""`         |
| `unreachCumulative` (keine Historie)                         | `"n.a."`     |

---

## 9. Caching & Nebenläufigkeit

- **TTL-Cache:** Die Antwort wird serverseitig bis zu **20 s** zwischengespeichert. Aufeinanderfolgende Abrufe innerhalb dieses Fensters liefern denselben (bis zu 20 s alten) Stand, ohne die Zentrale erneut abzufragen.
- **Single-Flight:** Pro Cache-Miss läuft höchstens **eine** Datenerhebung. Treffen mehrere Abrufe gleichzeitig ein, teilen sie sich das laufende Ergebnis; es werden keine parallelen Erhebungen gestartet.
- Die Werte spiegeln den zuletzt in der Zentrale bekannten Zustand der Sensoren wider (kein aktiver Funk-Poll der Geräte beim Abruf).

---

## 10. Response (HTTP)

### 10.1 Status Codes

| Code                      | Bedeutung                                           |
| ------------------------- | --------------------------------------------------- |
| `200 OK`                  | Telemetrie erfolgreich geliefert (Body = Datenbaum) |
| `401 Unauthorized`        | Fehlender oder ungültiger `Authorization`-Header    |
| `429 Too Many Requests`   | Rate Limit überschritten                            |
| `502 Bad Gateway`         | Telemetrie nicht lesbar (Zentrale nicht erreichbar) |
| `503 Service Unavailable` | Serverseitig kein API-Token konfiguriert            |

### 10.2 Success Response

Body ist der vollständige Telemetrie-Datenbaum (siehe Abschnitt 11).

### 10.3 Error Response

```json
{
  "error": "unauthorized"
}
```

```json
{
  "error": "telemetryUnavailable",
  "detail": "telemetry source returned HTTP 500"
}
```

| `error`                 | HTTP  | Bedeutung                               |
| ----------------------- | ----- | --------------------------------------- |
| `unauthorized`          | `401` | Token fehlt/falsch                      |
| `telemetryUnavailable`  | `502` | Datenerhebung fehlgeschlagen (`detail`) |
| `apiTokenNotConfigured` | `503` | Kein Token gesetzt                      |

---

## 11. Vollständiges Response-Beispiel

```json
{
  "timestamp": "2026-06-09T11:24:13Z",
  "fireStation": "Feuerwehr Feuerstadt, Hauptstr. 112, 01234 Feuerstadt",
  "deviceId": "001A2B3C4D5E6F",
  "objects": [
    {
      "type": "vehicle",
      "vehicleId": "WVWZZZ3CZWE123456",
      "sign": "FS-FW 112",
      "callSign": "1-HLF20-1",
      "vehicleType": "HLF20",
      "smokeDetectors": [
        {
          "name": "1-HLF20-1 RM1",
          "address": "00AABBCCDDEE11",
          "type": "SFHSS02",
          "version": "1",
          "group": "",
          "teams": "",
          "firmware": "1.0.6",
          "rssiDevice": "-65",
          "rssiPeer": "0",
          "battery": "false",
          "unreachState": "false",
          "unreachCumulative": "0",
          "operationTime": "180",
          "dirtLevel": "0.000000",
          "smokeLevel": "0.000000",
          "alarmState": "0",
          "voltage": "3.000000",
          "chamber": "false",
          "errorCode": "0"
        },
        {
          "name": "1-HLF20-1 RM2",
          "address": "00AABBCCDDEE22",
          "type": "SFHSS02",
          "version": "1",
          "group": "",
          "teams": "",
          "firmware": "1.0.6",
          "rssiDevice": "-72",
          "rssiPeer": "0",
          "battery": "false",
          "unreachState": "false",
          "unreachCumulative": "0",
          "operationTime": "180",
          "dirtLevel": "0.000000",
          "smokeLevel": "0.000000",
          "alarmState": "0",
          "voltage": "3.000000",
          "chamber": "false",
          "errorCode": "0"
        }
      ]
    }
  ]
}
```