# Zustandsdaten aus einem Batteriemanagementsystem senden

> Melden Sie Messwerte wie Ladezustand oder Vollzyklen mit updateCondition und lesen Sie, was übernommen, übersprungen und neu veröffentlicht wurde.

Ein Batteriemanagementsystem (BMS), ein Servicewerkzeug oder eine Monitoring-Plattform meldet seine Messwerte mit
[`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition). BatteryPass übernimmt die Werte als
Messungen ohne Prüfschritt, speichert sie mit Messzeitpunkt und `source` `api`, ergänzt den
Lebenszyklus der Einheit um ein Ereignis `condition_update`, das den Schlüssel nennt (`api_key`), und veröffentlicht den Pass nach der Regel Ihrer
Organisation neu. Die Grundlagen stehen unter [Zustand und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle).

## Bevor Sie beginnen

- **Ein Schlüssel mit dem Scope `condition`.** Eine Eigentümerin oder ein Eigentümer erstellt ihn im Portal
  unter **Einstellungen → API-Schlüssel**. Lesen (etwa
  [`getCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition) oder
  [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries)) braucht den Scope `read`; geben Sie dem
  Schlüssel beide Scopes, wenn das System beides tut. Mehr unter
  [Schlüssel sicher verwalten](https://passport.kabin.sh/docs/de/guides/secure-key-handling).
- **Die ID der Einheit.** Speichern Sie sie in Ihrem System neben der Seriennummer, etwa mit
  [Einheiten abgleichen](https://passport.kabin.sh/docs/de/guides/sync-units), oder suchen Sie sie mit `listBatteries?serial_prefix=`.
- **Die Felder.** Angenommen werden nur die dynamischen Felder des Feldregisters (Abschnitt `condition`), und
  nur die, die für die Batteriekategorie der Einheit gelten. `getCondition` nennt sie mit ihren aktuellen Werten
  und zeigt die Regel für die Neuveröffentlichung:

```bash title="cURL"
curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/condition" \
  -H "Authorization: Bearer $BATTERYPASS_API_KEY"
```

```json title="200 (gekürzt)"
{
  "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f",
  "field_keys": [
    "condition.state_of_charge",
    "condition.remaining_capacity",
    "condition.capacity_fade",
    "condition.number_of_full_cycles"
  ],
  "values": [
    {
      "field_key": "condition.number_of_full_cycles",
      "value": 412,
      "revision": 6,
      "source": "api",
      "review_status": "approved",
      "measured_at": "2026-10-23T06:00:00Z",
      "recorded_at": "2026-10-23T06:00:04Z",
      "registry_version": "0.5.0",
      "checks": []
    }
  ],
  "republish": "immediate"
}
```

## Ein Update senden

`PATCH /api/v1/batteries/{batteryId}/condition` mit einem JSON-Body:

- **values** `object[]` (body, erforderlich): 1 bis 50 Werte, jedes Feld höchstens einmal.

  **Wert**

  - **field_key** `string` (erforderlich): Ein dynamisches Feld der Einheit, z. B. `condition.state_of_charge`.
  - **value** `any` (erforderlich): Der Wert in der Einheit des Feldes (z. B. `%` oder `Ah`); `null` leert das Feld.
  - **measured_at** `date-time`: Wann dieser Wert gemessen wurde; ohne Angabe gilt `measured_at` des Updates.

* **measured_at** `date-time` (body): Der Messzeitpunkt aller Werte ohne eigenen.
* **note** `string` (body): Höchstens 500 Zeichen; wird die Beschreibung des Lebenszyklus-Ereignisses.

```bash title="cURL"
curl -sS -X PATCH "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/condition" \
  -H "Authorization: Bearer $BATTERYPASS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "measured_at": "2026-10-24T06:00:00Z",
    "note": "BMS daily report",
    "values": [
      { "field_key": "condition.state_of_charge", "value": 81.5 },
      { "field_key": "condition.number_of_full_cycles", "value": 412 },
      { "field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z" }
    ]
  }'
```

```ts title="TypeScript"
const batteryId = '0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f';
const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/batteries/${batteryId}/condition`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    measured_at: '2026-10-24T06:00:00Z',
    note: 'BMS daily report',
    values: [
      { field_key: 'condition.state_of_charge', value: 81.5 },
      { field_key: 'condition.number_of_full_cycles', value: 412 },
      { field_key: 'condition.remaining_capacity', value: 187.2, measured_at: '2026-10-24T05:40:00Z' },
    ],
  }),
});
const result = await res.json();
if (!res.ok) throw new Error(`${res.status} ${result.code} (request_id ${result.request_id})`);
console.log(result.publication.outcome, result.skipped);
```

```python title="Python"
import os

import requests

battery_id = "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f"
res = requests.patch(
    f"{os.environ['BATTERYPASS_URL']}/api/v1/batteries/{battery_id}/condition",
    headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"},
    json={
        "measured_at": "2026-10-24T06:00:00Z",
        "note": "BMS daily report",
        "values": [
            {"field_key": "condition.state_of_charge", "value": 81.5},
            {"field_key": "condition.number_of_full_cycles", "value": 412},
            {"field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z"},
        ],
    },
    timeout=30,
)
result = res.json()
if not res.ok:
    raise SystemExit(f"{res.status_code} {result['code']} (request_id {result.get('request_id')})")
print(result["publication"]["outcome"], result["skipped"])
```

```go title="Go"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	batteryID := "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f"
	body, _ := json.Marshal(map[string]any{
		"measured_at": "2026-10-24T06:00:00Z",
		"note":        "BMS daily report",
		"values": []map[string]any{
			{"field_key": "condition.state_of_charge", "value": 81.5},
			{"field_key": "condition.number_of_full_cycles", "value": 412},
			{"field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z"},
		},
	})
	url := os.Getenv("BATTERYPASS_URL") + "/api/v1/batteries/" + batteryID + "/condition"
	req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	var result struct {
		Skipped     []map[string]string `json:"skipped"`
		Publication struct {
			Outcome string `json:"outcome"`
		} `json:"publication"`
		Code      string `json:"code"`
		RequestID string `json:"request_id"`
	}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		log.Fatal(err)
	}
	if res.StatusCode != http.StatusOK {
		log.Fatalf("%d %s (request_id %s)", res.StatusCode, result.Code, result.RequestID)
	}
	fmt.Println(result.Publication.Outcome, result.Skipped)
}
```

```json title="200 (gekürzt)"
{
  "accepted": [
    {
      "field_key": "condition.state_of_charge",
      "value": 81.5,
      "unit": "%",
      "revision": 9,
      "source": "api",
      "review_status": "approved",
      "measured_at": "2026-10-24T06:00:00Z",
      "recorded_at": "2026-10-24T06:00:03Z",
      "reviewed_at": "2026-10-24T06:00:03Z",
      "registry_version": "0.5.0",
      "checks": []
    },
    {
      "field_key": "condition.remaining_capacity",
      "value": 187.2,
      "unit": "Ah",
      "revision": 5,
      "source": "api",
      "review_status": "approved",
      "measured_at": "2026-10-24T05:40:00Z",
      "recorded_at": "2026-10-24T06:00:03Z",
      "reviewed_at": "2026-10-24T06:00:03Z",
      "registry_version": "0.5.0",
      "checks": []
    }
  ],
  "skipped": [
    { "field_key": "condition.number_of_full_cycles", "reason": "unchanged" }
  ],
  "event": {
    "id": "0192f8b1-2a3c-7d4e-9f50-61a2b3c4d5e6",
    "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f",
    "type": "condition_update",
    "occurred_on": "2026-10-24",
    "description": "BMS daily report",
    "source": "api",
    "api_key": { "id": "0192e4c7-8b9a-7c6d-8e5f-4a3b2c1d0e9f", "name": "BMS Landshut" },
    "field_keys": ["condition.state_of_charge", "condition.remaining_capacity"],
    "evidence": [],
    "created_at": "2026-10-24T06:00:03Z"
  },
  "publication": {
    "outcome": "published",
    "version": {
      "version_no": 4,
      "published_at": "2026-10-24T06:00:03Z",
      "reason": "condition update",
      "registry_version": "0.5.0",
      "canonical_hash": "sha256:5d0c9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d",
      "public_hash": "sha256:7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d5d0c9e8f7a6b5c4d3e2f1a0b9c8d",
      "key_id": "2026-10-k1",
      "batterypass_v2_valid": true,
      "batterypass_v2_issue_count": 0
    }
  }
}
```

## Das Ergebnis lesen

`accepted` enthält die gespeicherten Werte mit `source: api`, `review_status: approved` und ihrem
`measured_at`. `skipped` nennt Werte, die gültig waren, aber nicht gespeichert wurden:

| `reason`            | Bedeutung                                                                                                        |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `older_measurement` | Der gespeicherte Wert wurde später gemessen als der gesendete.                                                   |
| `unchanged`         | Der gespeicherte Wert hat denselben Wert und denselben Messzeitpunkt, oder Sie haben ein Feld ohne Wert geleert. |

`event` ist das Ereignis `condition_update` im Lebenszyklus der Einheit; es fehlt, wenn nichts übernommen wurde.
Sein `occurred_on` ist der UTC-Tag der jüngsten übernommenen Messung, und `note` wird seine `description`.

`publication.outcome` sagt, was mit dem Pass geschah:

| `outcome`       | Bedeutung                                                                                                                                                                          |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `published`     | Eine neue Passversion mit dem Grund `condition update` ist jetzt online (`version`).                                                                                               |
| `unchanged`     | Sofort veröffentlicht, aber der öffentliche Inhalt hat sich nicht geändert; es entstand keine neue Version.                                                                        |
| `scheduled`     | Ihre Organisation veröffentlicht täglich: Der Lauf um 02:00 UTC übernimmt die neuen Werte.                                                                                         |
| `blocked`       | Die Veröffentlichungsprüfung hat abgelehnt (`blockers`). Die Werte sind gespeichert; der Pass bleibt im Status `update_needed` online, bis ein Mitglied ihn erneut veröffentlicht. |
| `not_published` | Die Einheit hat keinen öffentlichen Pass; die Werte erscheinen mit der ersten Veröffentlichung.                                                                                    |

Die Regel (`immediate` oder `daily`) ist eine Einstellung der Organisation im Portal; `getCondition` zeigt sie
als `republish`. Die meisten Zustandsfelder gehören zur Zugriffsstufe `legitimate_interest`: Sie stehen im
signierten Snapshot und in den eingeschränkten Ansichten, während das öffentliche JSON nur ihre Schlüssel in
`restricted_fields_present` nennt (siehe [Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers)).

## Regeln für ein Update

- **Merge.** Nur die genannten Felder ändern sich; alle anderen behalten ihren Wert. `null` leert ein Feld.
- **Messzeitpunkt.** Jeder Wert braucht ein `measured_at`, ein eigenes oder das der Anfrage. Es darf höchstens
  fünf Minuten nach der Serveruhr liegen und nicht vor dem Herstellungsdatum der Einheit. Senden Sie es in UTC
  (`Z`).
- **Die jüngste Messung gewinnt.** Ein Wert, der früher gemessen wurde als der gespeicherte, wird übersprungen;
  verspätete oder doppelte Lieferungen richten also keinen Schaden an. Dasselbe Update zweimal zu senden ist
  unbedenklich: Beim zweiten Mal kommt alles als `unchanged` zurück.
- **Alles oder nichts.** Ist ein Wert ungültig oder verletzen die zusammengeführten Zustandsdaten eine
  feldübergreifende Prüfung des Registers, wird das ganze Update abgelehnt und nichts gespeichert.
- **Werte, keine Dokumente.** `updateCondition` nimmt keine `evidence_ids`; einen Diagnosebericht hängen Sie an
  ein [Lebenszyklus-Ereignis](https://passport.kabin.sh/docs/de/guides/record-lifecycle-events).

## Fehler

```json title="400"
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "code": "validation_error",
  "detail": "The request failed validation.",
  "request_id": "4f9c2a7d1e0b5c8a3f6e2d19",
  "errors": [
    { "field": "values[0].field_key", "code": "unknown_field", "message": "performance.nominal_voltage is not condition data of this unit" },
    { "field": "values[1].measured_at", "code": "out_of_range", "message": "measured_at lies in the future" }
  ]
}
```

| Status und `code`      | Typische Ursache                                                                                                                                                                                                                                                              | Was zu tun ist                                    |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `400 validation_error` | `values[i].field_key` mit `unknown_field` (kein dynamisches Feld dieser Einheit); `values[i].measured_at` mit `required` oder `out_of_range`; `values[i].value` außerhalb des Wertebereichs; ein Feld doppelt; eine fehlgeschlagene feldübergreifende Prüfung; `note` zu lang | Daten korrigieren; nicht unverändert wiederholen  |
| `401 unauthenticated`  | Schlüssel fehlt, ist unbekannt oder widerrufen                                                                                                                                                                                                                                | Schlüssel im Secret-Speicher prüfen               |
| `403 forbidden`        | Dem Schlüssel fehlt der Scope `condition`                                                                                                                                                                                                                                     | Einen Schlüssel mit diesem Scope erstellen lassen |
| `404 not_found`        | Die Einheit gibt es in der Organisation des Schlüssels nicht                                                                                                                                                                                                                  | ID prüfen                                         |
| `409 conflict`         | Das Batteriemodell der Einheit ist archiviert: Seine Daten lassen sich lesen, aber nicht ändern                                                                                                                                                                               | Für Einheiten dieses Modells nichts mehr melden   |
| `429 rate_limited`     | Mehr als 600 Anfragen pro Minute über alle Schlüssel der Organisation                                                                                                                                                                                                         | `Retry-After` abwarten, dann erneut senden        |

[Fehler und Wiederholungen](https://passport.kabin.sh/docs/de/guides/handle-errors) zeigt eine Hilfsfunktion für Wiederholungen; das Rezept
[Nächtlicher Zustandsdaten-Push](https://passport.kabin.sh/docs/de/recipes/nightly-condition-push) sendet ein Update je Einheit mit
Backoff.
