# Neuen Displays automatisch ein Programm geben

Am Ende bekommt jedes neue Display von selbst das Programm seines Standorts, wenige Sekunden nachdem es in der Flotte erscheint. Zieht ein Display an einen anderen Standort um, übernimmt es dessen Programm. Nach einem Rollout muss niemand mehr daran denken, Inhalte zuzuweisen.

> **Typ:** anleitung
> **Für wen:** Entwicklerinnen und Entwickler, die viele Standorte betreuen oder Displays in Wellen ausrollen
> **Voraussetzung:** ein Paket mit Webhooks (ab Premium), die Rolle Inhaber für den Endpunkt, ein Schlüssel `ak_…` der Klasse **Individuell** mit `fleet.player.read` und `schedule.update` und ein Server mit öffentlicher https-Adresse
> **Ort:** Ihr eigener Server und **Einstellungen › Für Entwickler › Webhooks**

## So funktioniert es

| Ereignis | Wann es kommt | Was der Empfänger tut |
|---|---|---|
| `fleet.player.created` | Ein Display ist entstanden, als Web-Display oder durch Koppeln eines Geräts | Seinen Standort lesen und das Programm dieses Standorts zuweisen. |
| `fleet.player.updated` mit `locationId` in `changedFields` | Ein Display ist an einen anderen Standort umgezogen | Das Programm des neuen Standorts zuweisen. Das ersetzt die bisherige Zuweisung. |

Der Empfänger führt eine Tabelle, welcher Standort welches Programm bekommt, dazu eine Vorgabe für Displays ohne Standort.

## Schritte

1. **Schreiben Sie die Tabelle** aus Standorten und Programmen als JSON auf. Die Standort-Kennungen liefert `GET /v1/locations`, die Programm-Kennungen stehen in der Portal-Adresse:

   ```json
   {
     "loc_01KFB7QPW0LOCATION00000001": "sch_01KWVAFRM0ZETPAN0000000001",
     "loc_01KFB7QPW0LOCATION00000002": "sch_01KWVAFRM0ZETPAN0000000002",
     "default": "sch_01KWVAFRM0ZETPAN0000000003"
   }
   ```

2. **Speichern Sie den Empfänger:**

   ```ts
   // auto-assign.ts: node auto-assign.ts
   import { createServer } from 'node:http';
   import { createHmac, timingSafeEqual } from 'node:crypto';
   import { readFileSync } from 'node:fs';

   const SECRET = process.env.WEBHOOK_SECRET ?? ''; // whsec_…
   const API = process.env.SZENERA_API;
   const KEY = process.env.SZENERA_API_KEY; // fleet.player.read und schedule.update
   const PROGRAMS: Record<string, string> = JSON.parse(readFileSync('programs.json', 'utf8'));
   const seen = new Set<string>();

   function isFromSignage(rawBody: string, header: string): boolean {
     const expected = createHmac('sha256', SECRET).update(rawBody).digest('base64url');
     const a = Buffer.from(expected);
     const b = Buffer.from(header);
     return a.length === b.length && timingSafeEqual(a, b);
   }

   async function call(method: string, path: string, body?: unknown) {
     const res = await fetch(`${API}${path}`, {
       method,
       headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
       body: body === undefined ? undefined : JSON.stringify(body),
     });
     if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
     return res.json();
   }

   async function assignProgramOfLocation(playerId: string): Promise<void> {
     const { player } = await call('GET', `/v1/players/${playerId}`);
     const scheduleId = PROGRAMS[player.locationId ?? ''] ?? PROGRAMS.default;
     if (!scheduleId) return;
     await call('PUT', `/v1/players/${playerId}/program`, {
       program: { kind: 'schedule', scheduleId },
     });
     console.log(`${player.name}: ${scheduleId}`);
   }

   createServer((req, res) => {
     const chunks: Buffer[] = [];
     req.on('data', (chunk) => chunks.push(chunk));
     req.on('end', async () => {
       const raw = Buffer.concat(chunks).toString('utf8');
       if (!isFromSignage(raw, String(req.headers['x-signature'] ?? ''))) {
         res.writeHead(401).end();
         return;
       }
       res.writeHead(204).end(); // Binnen 10 Sekunden antworten, danach arbeiten

       const deliveryId = String(req.headers['x-delivery-id']);
       if (seen.has(deliveryId)) return;
       seen.add(deliveryId);

       const event = req.headers['x-event-type'];
       const payload = JSON.parse(raw);
       const moved = event === 'fleet.player.updated' && payload.changedFields?.includes('locationId');
       if (event === 'fleet.player.created' || moved) {
         await assignProgramOfLocation(payload.playerId).catch((error) => console.error(error));
       }
     });
   }).listen(8080);
   ```

3. **Legen Sie den Endpunkt an,** im Portal unter **Einstellungen › Für Entwickler › Webhooks** mit **Endpunkt anlegen**. Tragen Sie Ihre https-Adresse ein, wählen Sie unter **Abonnierte Ereignisse** `fleet.player.created` und `fleet.player.updated` und klicken Sie auf **Anlegen**. Kopieren Sie das Secret nach `WEBHOOK_SECRET` und starten Sie den Empfänger.

4. **Probieren Sie es aus** mit einem neuen Web-Display, siehe [Ein Display anlegen und bespielen](/de/recipes/create-web-display), aber ohne Programm zuzuweisen. Wenige Sekunden später nennt `GET /v1/players/{id}/program` das Programm seines Standorts.

Dasselbe Programm zweimal zuzuweisen schadet nicht. Eine wiederholte Zustellung richtet also auch nach einem Neustart des Empfängers keinen Schaden an.

## Ergebnis prüfen

- Der Empfänger schreibt je neuem Display eine Zeile mit Namen und Programm.
- Im Portal zeigt das neue Display das Programm seines Standorts.
- Im **Zustell-Log** des Endpunkts zeigt jede Zustellung **Zugestellt** mit `HTTP 204`.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| Der Empfänger meldet `403` | Dem Schlüssel fehlt `fleet.player.read` oder `schedule.update`. | Einen Schlüssel der Klasse **Individuell** mit beiden Rechten erzeugen. |
| Neue Displays bekommen das Vorgabe-Programm | Sie haben beim Anlegen noch keinen Standort. | Den Standort im Portal zuordnen. Der Umzug löst `fleet.player.updated` aus, und der Empfänger weist das richtige Programm zu. |
| Ein von Hand gewähltes Programm wurde ersetzt | Das Display ist an einen anderen Standort umgezogen. | `fleet.player.updated` aus den abonnierten Ereignissen nehmen, wenn ein Umzug das Programm behalten soll. |
| Das Zustell-Log zeigt `HTTP 401` | Das Secret passt nicht. | Mit **Secret rotieren** ein neues ausstellen und setzen, siehe [Die Signatur einer Zustellung prüfen](/de/webhooks/verify-signature). |

## Weiterführendes

- [Ein Display anlegen und bespielen](/de/recipes/create-web-display)
- [Bei Offline eine Nachricht in Teams oder Slack](/de/recipes/offline-alert-chat)
- [Ereignistypen: Inhalte, Programme, Geräte, Apps](/de/webhooks/event-types-content-devices)
