Zum Inhalt springen

Wiederholungen und Idempotenz

Welche Anfragen Sie nach einem Timeout gefahrlos wiederholen können und wie Sie doppelte Ereignisse vermeiden.

Wenn eine Anfrage in einen Timeout läuft oder die Verbindung abbricht, wissen Sie nicht, ob BatteryPass sie ausgeführt hat. Ob Sie sie einfach erneut senden können, hängt von der Operation ab.

Überblick

OperationenMethodeGefahrlos wiederholbarWarum
Alle Lesezugriffe, auch öffentliche Abrufe von PässenGETJaSie ändern nichts.
updateConditionPATCHJa, mit demselben measured_atEine Messung, die gleich alt oder älter als die gespeicherte ist, wird übersprungen und aufgeführt.
createLifecycleEventPOSTNeinJeder erfolgreiche Aufruf erfasst ein neues Ereignis.

Eine mit 429 beantwortete Anfrage wurde nicht ausgeführt und lässt sich nach Retry-After immer wiederholen (siehe Ratenlimits). Eine Antwort 4xx heißt, dass die Anfrage als Ganzes abgelehnt wurde: Korrigieren Sie sie, statt sie zu wiederholen.

Lesezugriffe

Jedes GET der API können Sie beliebig oft wiederholen. Wiederholen Sie Netzwerkfehler und Antworten 5xx mit exponentiellem Backoff.

Zustandsdaten

updateCondition führt Messwerte mit den Zustandsdaten der Einheit zusammen. Für jeden Wert vergleicht BatteryPass sein measured_at mit dem des gespeicherten Werts:

  • neuer als die gespeicherte Messung oder gleicher Zeitpunkt mit anderem Wert: gespeichert, aufgeführt in accepted;
  • gleicher Zeitpunkt und gleicher Wert: übersprungen mit dem Grund unchanged;
  • älter als die gespeicherte Messung: übersprungen mit dem Grund older_measurement.

Eine wiederholte Anfrage mit demselben Body ändert also nichts. Senden Sie zum Beispiel:

Request
{
  "measured_at": "2026-10-24T02:00:00Z",
  "values": [
    { "field_key": "condition.state_of_charge", "value": 87.5 },
    { "field_key": "condition.number_of_full_cycles", "value": 412 }
  ]
}

Wurde der erste Versuch gespeichert, antwortet die Wiederholung:

200
{
  "accepted": [],
  "skipped": [
    { "field_key": "condition.state_of_charge", "reason": "unchanged" },
    { "field_key": "condition.number_of_full_cycles", "reason": "unchanged" }
  ],
  "publication": { "outcome": "not_published" }
}

Diese Anfrage erfasst kein neues Lebenszyklusereignis und veröffentlicht nichts: not_published heißt hier, dass diese Anfrage nichts veröffentlicht hat; der Pass behält seine aktuelle Version.

Setzen Sie measured_at auf den Zeitpunkt der Messung, zum Beispiel den Zeitpunkt, zu dem Ihr Batteriemanagementsystem den Wert erfasst hat, und behalten Sie ihn bei einer Wiederholung bei. Ein measured_at aus der Uhrzeit beim Senden macht jede Wiederholung zu einer neuen Messung.

Lebenszyklusereignisse

createLifecycleEvent erkennt keine Duplikate: Jeder erfolgreiche Aufruf erfasst ein neues Ereignis, und die veröffentlichte API kennt keinen Idempotenzschlüssel. Prüfen Sie nach einem Timeout, einem Verbindungsabbruch oder einer Antwort 5xx, ob das Ereignis angekommen ist, bevor Sie es erneut senden:

  1. Lesen Sie listLifecycleEvents für die Einheit (neueste zuerst).
  2. Suchen Sie ein Ereignis mit source: "api" und Ihrem type, occurred_on und description.
  3. Senden Sie das Ereignis nur erneut, wenn es fehlt.

Schreiben Sie eine eigene Referenz in description, etwa die Nummer des Serviceauftrags, damit sich das Ereignis leicht wiederfinden lässt. BatteryPass speichert die Beschreibung ohne führende und nachfolgende Leerzeichen.

type NewEvent = { type: string; occurred_on: string; description: string };
type LifecycleEvent = NewEvent & { id: string; source: string };

const base = `${process.env.BATTERYPASS_URL}/api/v1`;
const headers = {
  Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`,
  'Content-Type': 'application/json',
};

// Sucht ein Ereignis, das diese Integration schon erfasst hat (gleicher Typ, Tag und Beschreibung).
async function findEvent(batteryId: string, e: NewEvent): Promise<LifecycleEvent | undefined> {
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({ limit: '200' });
    if (cursor) query.set('cursor', cursor);
    const res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events?${query}`, { headers });
    if (!res.ok) throw new Error(`listLifecycleEvents: ${res.status}`);
    const page: { items: LifecycleEvent[]; next_cursor: string | null } = await res.json();
    const hit = page.items.find(
      (x) => x.source === 'api' && x.type === e.type && x.occurred_on === e.occurred_on && x.description === e.description,
    );
    if (hit) return hit;
    // Neueste zuerst: Sobald eine Seite ältere Tage erreicht, ist das Ereignis nicht da.
    if (page.items.some((x) => x.occurred_on < e.occurred_on)) return undefined;
    cursor = page.next_cursor;
  } while (cursor);
  return undefined;
}

async function recordEvent(batteryId: string, e: NewEvent): Promise<LifecycleEvent> {
  for (let attempt = 1; attempt <= 3; attempt++) {
    let res: Response | undefined;
    try {
      res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events`, {
        method: 'POST',
        headers,
        body: JSON.stringify(e),
        signal: AbortSignal.timeout(30_000),
      });
    } catch {
      res = undefined; // Timeout oder Verbindungsfehler
    }
    if (res?.status === 201) return res.json();
    if (res && res.status < 500) throw new Error(`createLifecycleEvent: ${res.status} ${await res.text()}`);
    // Keine Antwort oder 5xx: Das Ereignis ist vielleicht trotzdem erfasst.
    const existing = await findEvent(batteryId, e);
    if (existing) return existing;
  }
  throw new Error('createLifecycleEvent: nach 3 Versuchen kein Erfolg');
}

const event = await recordEvent('0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12', {
  type: 'inspection',
  occurred_on: '2026-10-24',
  description: 'Serviceauftrag SO-2026-0815: jährliche Inspektion',
});
console.log(event.id);

Die Beispiele behandeln ein 429 wie jede andere Ablehnung; kombinieren Sie sie im Betrieb mit dem Backoff aus Ratenlimits.

Der Header Idempotency-Key

BatteryPass kennt einen Header Idempotency-Key, aber nur bei Operationen des Portals wie dem Veröffentlichen und Massenjobs. Keine der veröffentlichten Operationen akzeptiert ihn; senden Sie ihn also nicht an die API, dort bewirkt er nichts.

Auf dieser Seite