hDP Protokoll-Spezifikationen

homeESS Device Protocol (HDP) 1.0-draft

1. Zweck und Geltungsbereich

Dieses Dokument ist der vollständige normative Kommunikationsvertrag zwischen einem homeESS-Adapter (im Folgenden Adapter) und einer HDP-Firmware auf einem lokalen Endgerät (im Folgenden Gerät).

Es definiert ausschließlich:

  • Erkennung über mDNS/DNS-SD,
  • HTTP-Endpunkte und JSON-Strukturen,
  • Kopplung und Authentifizierung,
  • Hardwarekonfiguration und generische Ausgänge,
  • WebSocket-Sitzungen, Pixel-Frames, Timelineprogramme und Binary-I/O-Ereignisse,
  • Firmwareübertragung per OTA,
  • Zustände, Timeouts, Wiederholungen und Fehlerbehandlung.

Projektorganisation, Quellcodeaufbau, Releaseprozess, Roadmap und Änderungsverlauf gehören ausdrücklich nicht in diesen Vertrag.

Die Schlüsselwörter MUSS, DARF NICHT, SOLL, SOLL NICHT und DARF sind im Sinn von RFC 2119 zu verstehen. Ein Teilnehmer ist nur dann HDP-1.0-draft-konform, wenn er alle MUSS-Anforderungen dieses Dokuments erfüllt.

1.1 Verbindliche Zuständigkeitsgrenze

Der Adapter beziehungsweise das zugehörige Plugin ist allein verantwortlich für:

  • die anwendungsspezifische Bedeutung von device_type und aller Topics;
  • Datenquellen, Prozent-, Richtungs-, Schwellen- und Anzeigelogik;
  • die Berechnung jedes logischen RGB-Pixelwerts;
  • Farben, Übergänge, Effekte und Animationen;
  • die Übersetzung dieser Ergebnisse in vollständige Frames, Pixeländerungen oder

ein Timelineprogramm nach Abschnitt 14.

Das Gerät ist allein verantwortlich für:

  • Pairing, Binding, Authentifizierung und Sitzungsverwaltung;
  • Persistierung und Validierung der physischen Hardwarekonfiguration;
  • Auswahl der durch device_type festgelegten generischen Laufzeitschicht beim

Booten sowie Ansteuerung der konfigurierten Ein- und Ausgänge;

  • zeitgesteuerte Wiedergabe bereits berechneter Timelineereignisse;
  • Durchsetzung der deklarierten Größen-, Timing-, Helligkeits- und Stromgrenzen;
  • sichere Offline-, Neustart- und Fehlerzustände.

Das Gerät DARF device_type ausschließlich zur Auswahl des normierten Laufzeitprofils und seiner physischen Treiberschicht auswerten. Es DARF keine Prozentanzeige, Richtungsanzeige, Topic-Logik, Farbauswahl, Toggle-, Set- oder Counterlogik und keine anwendungsspezifische Animation selbst erzeugen. Ein Timeline-Decoder, Pixelpuffer, Scheduler, Binary-Pin-Treiber, Entprellung und elektrische Schutzfunktionen gelten als generische Geräteschicht.

2. Protokollidentität und Kompatibilität

Der exakte Protokollbezeichner lautet:

1.0-draft

Adapter und Gerät MÜSSEN diesen Wert unverändert in mDNS, /device, /manifest, Pairing und WebSocket verwenden. Dieser Draft definiert vier Laufzeitprofile:

pixel-timeline-v1
binary-io-v1
sensor-reading-v1
fingerprint-event-v1

percentage_indicator und argb_output verwenden pixel-timeline-v1; binary_io verwendet binary-io-v1, sensors verwendet sensor-reading-v1, fingerprint_reader verwendet fingerprint-event-v1. Mehrere Gerätetypen DÜRFEN sich ein Laufzeitprofil teilen: Das Profil beschreibt ausschließlich die Ausgabeschicht des Geräts, nicht die Bedeutung, die der Adapter den Bildern gibt. Das aktive Profil wird in mDNS, /device, /manifest und im WebSocket-Hello ausgetauscht und MUSS an allen vier Stellen identisch sein. Dadurch bleibt das bereits persistierte HDP-1.0-Pairing unverändert, während experimentelle Firmware- und Adapterstände mit unterschiedlicher Ausgabeschicht keine scheinbar kompatible Steuersitzung aufbauen.

Für 1.0-draft gilt:

  • unbekannte JSON-Felder MÜSSEN ignoriert werden;
  • fehlende Pflichtfelder MÜSSEN abgelehnt werden;
  • unbekannte Enum-Werte MÜSSEN abgelehnt werden;
  • unbekannte WebSocket-Nachrichtentypen MÜSSEN mit UNSUPPORTED_MESSAGE_TYPE

beantwortet werden;

  • ein anderer Protokollbezeichner MUSS mit UNSUPPORTED_PROTOCOL_VERSION

abgelehnt werden;

  • stillschweigende Typkonvertierung, etwa String nach Integer, ist nicht erlaubt.

Alle in Request- und Response-Beispielen gezeigten Felder sind Pflichtfelder, sofern ihre Optionalität oder bedingte Nullbarkeit nicht ausdrücklich beschrieben ist. Ein Sender DARF zusätzliche Felder ergänzen; ein Empfänger DARF deren Vorhandensein nicht voraussetzen.

Solange der Bezeichner 1.0-draft trägt und noch nicht als stabile Version 1.0 veröffentlicht wurde, darf dieser Vertrag weiter präzisiert werden. Eine inkompatible Änderung der Laufzeitausgabe MUSS dabei mindestens ein neues runtime_profile erhalten. Pairing-, Binding-, Authentifizierungs- und Hashregeln DÜRFEN dadurch nicht stillschweigend verändert werden. Nach Veröffentlichung der stabilen Version 1.0 erfordert jede inkompatible Änderung einen neuen Protokollbezeichner.

3. Transport und Kodierung

3.1 Netzwerkprofil

HDP 1.0-draft verwendet ein lokales IPv4-Netzwerk.

KanalTransport
DiscoverymDNS/DNS-SD
Metadaten und SteuerungHTTP/1.1
Laufzeitwerte und EreignisseWebSocket nach RFC 6455
FirmwarebinärdatenHTTP/1.1 Streaming

TLS ist in diesem Profil nicht vorgeschrieben. Der binding_key wird deshalb nur in einem vertrauenswürdigen lokalen Netz übertragen. Ein Gerät DARF dieses Profil nicht über das öffentliche Internet exponieren.

WLAN-Provisionierung und Captive-Portal-HTML sind nicht Bestandteil von HDP 1.0-draft. Sie erfolgen vor der Discovery über einen gerätespezifischen, außerhalb dieses Vertrags liegenden Kanal. Ein Adapter DARF keine Portal-Route als HDP-API behandeln.

3.2 HTTP

  • Basis-URI: /api/v1
  • Pfade sind case-sensitive und werden ohne abschließenden Slash verwendet.
  • HDP 1.0-draft definiert keine Query-Parameter.
  • JSON-Encoding: UTF-8 ohne BOM
  • JSON-Request-Content-Type: application/json; optional ist ausschließlich der

Parameter charset=utf-8, Groß-/Kleinschreibung ist nicht relevant

  • JSON-Response-Content-Type: application/json
  • Binär-Content-Type: application/octet-stream
  • jede JSON-Response MUSS Cache-Control: no-store enthalten
  • Headernamen sind nach HTTP/1.1 case-insensitive; alle in diesem Vertrag als

„exakt“ bezeichneten Headerwerte sind case-sensitive.

  • JSON- und Binärrequests MÜSSEN Content-Length senden; HTTP Chunked Transfer

Encoding ist für Request-Bodies nicht zulässig.

  • API-Endpunkte DÜRFEN nicht auf Portal- oder HTML-Seiten umleiten.
  • Ein JSON-Request-Body darf höchstens 3072 Bytes groß sein.
  • Ein zu großer Body MUSS mit HTTP 413 und PAYLOAD_TOO_LARGE beantwortet werden.
  • Ein JSON-Body MUSS ein Objekt als Wurzel besitzen.
  • Nicht endliche Zahlen (NaN, Infinity) sind ungültig.
  • Ein fehlender oder falscher Content-Type bei einem Request mit vorgeschriebenem

JSON-Body ergibt HTTP 415 und UNSUPPORTED_MEDIA_TYPE.

  • Ein unbekannter API-Pfad ergibt HTTP 404 und ENDPOINT_NOT_FOUND.
  • Eine falsche Methode auf einem bekannten API-Pfad ergibt HTTP 405 und

METHOD_NOT_ALLOWED.

Verletzt ein Request mehrere Regeln gleichzeitig, DARF das Gerät den zuerst erkannten passenden Fehler aus Abschnitt 16 zurückgeben. Ein Adapter DARF sich nicht auf die Fehlerpriorität mehrfach ungültiger Requests verlassen.

3.3 JSON-Datentypen

NotationBedeutung
stringUTF-8-String
uint8JSON-Integer 0…255
uint16JSON-Integer 0…65535
uint32JSON-Integer 0…4294967295
numberendliche JSON-Zahl
booleanausschließlich true oder false

3.4 Gemeinsames Erfolgs-Envelope

Jede erfolgreiche JSON-Antwort MUSS diese Form verwenden:

{
  "ok": true,
  "data": {}
}

data MUSS vorhanden sein und ein Objekt sein.

3.5 Gemeinsames Fehler-Envelope

Jede fehlgeschlagene JSON-Antwort MUSS diese Form verwenden:

{
  "ok": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Human-readable diagnostic",
    "details": {}
  }
}
  • code MUSS einem Code aus Abschnitt 16 entsprechen.
  • message dient Diagnosezwecken und DARF nicht maschinell ausgewertet werden.
  • details MUSS vorhanden und ein Objekt sein.
  • WLAN-Passwörter und Binding-Keys DÜRFEN nie in Responses, Discovery oder Logs

enthalten sein.

  • instance_id DARF nur an den in diesem Vertrag ausdrücklich gezeigten Stellen

übertragen werden und DARF nicht in Fehlerdetails, Discovery oder Gerätelogs erscheinen.

4. Identifikatoren und kryptografische Werte

4.1 device_id

  • dauerhaft pro Gerät;
  • 12…64 Zeichen;
  • erlaubte Zeichen: a-z, 0-9, -;
  • vom Gerät bei der Erstinbetriebnahme zufällig erzeugt;
  • enthält mindestens 64 Bit Zufallsentropie;
  • bleibt bei Neustart, Entkopplung und OTA erhalten;
  • wird bei einem erfolgreichen Factory Reset verworfen und danach neu erzeugt;
  • ist für den Adapter ein opaker Wert und DARF nicht aus MAC-Adresse, Hostname oder

anderen Eigenschaften abgeleitet werden.

Beispiel:

hdp-esp8266-a1b2c3d4e5f60718

4.2 instance_id

  • dauerhaft pro homeESS-Installation;
  • 1…64 ASCII-Zeichen;
  • erlaubte Zeichen: A-Z, a-z, 0-9, ., _, -;
  • Doppelpunkt ist nicht erlaubt.

4.3 Nonces und Pairing-Session

adapter_nonce, device_nonce und pairing_session sind jeweils 16 zufällige Bytes, kodiert als exakt 32 lowercase Hexzeichen.

Regulärer Ausdruck:

^[0-9a-f]{32}$

Sie MÜSSEN mit einem kryptografisch geeigneten Zufallszahlengenerator erzeugt werden.

4.4 binding_key

Der Adapter erzeugt 32 zufällige Bytes und kodiert sie als exakt 64 lowercase Hexzeichen:

^[0-9a-f]{64}$

Der gleiche 64-stellige String MUSS atomar mit der Kopplung auf Adapter und Gerät gespeichert werden. Der Wert ist geheim und DARF nach erfolgreicher Kopplung nie in einer Response, Discovery-Antwort oder einem Log erscheinen.

4.5 binding_id

Die Bildung ist bytegenau festgelegt:

  1. Den 64-stelligen binding_key als Hex dekodieren.
  2. Dadurch exakt 32 Bytes erhalten.
  3. SHA-256 über diese 32 Bytes berechnen.
  4. Den 32-Byte-Digest als exakt 64 lowercase Hexzeichen kodieren.

Formal:

binding_id = lowercase_hex(SHA-256(hex_decode(binding_key)))

Es werden ausdrücklich nicht die 64 ASCII-Zeichen des Hexstrings gehasht.

binding_id ist ein nicht geheimes Vergleichsmerkmal und DARF öffentlich übertragen werden.

Verbindliche Testvektoren:

binding_keybinding_id
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaae0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e
000000000000000000000000000000000000000000000000000000000000000066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef4884fdaafea47c29fea7159d0daddd9c085d6200e1359e85bb81736af6b7c837

4.6 message_id

  • 1…64 Zeichen;
  • innerhalb einer WebSocket-Verbindung pro Sender eindeutig;
  • erlaubte Zeichen: A-Z, a-z, 0-9, ., _, -.

5. Persistente Zustände und Invarianten

Das Gerät MUSS mindestens folgende Werte persistent speichern:

  • device_id,
  • WLAN-Zugangsdaten,
  • owner_instance_id,
  • binding_key,
  • Hardwarekonfiguration,
  • Hardwarekonfigurationsrevision.

Eine aktive Kopplung existiert genau dann, wenn:

owner_instance_id gültig
UND
binding_key gültig

Ein Teilnehmer DARF paired=true nicht allein aus einer Owner-ID, einem lokalen Datenbankeintrag oder einer früheren Discovery-Antwort ableiten.

Die Hardwarekonfiguration MUSS Neustart, WLAN-Ausfall, Adapterausfall, Entkopplung und OTA überstehen. Nur Factory Reset löscht sie.

Eine Hardware-Schemamigration oder Änderung des Laufzeitprofils DARF device_id, WLAN-Zugangsdaten, owner_instance_id oder binding_key weder löschen noch verändern. Pixelpuffer, laufende oder hochgeladene Timelines und deren Upload-Staging sind flüchtige Laufzeitdaten und gehören nicht zur persistenten Hardwarekonfiguration.

6. Zustandsmodelle

6.1 Betriebszustand

GET /status verwendet ausschließlich folgende lowercase Werte:

ZustandBedeutung
access_pointSetup-AP aktiv
pairableWLAN aktiv, nicht gekoppelt
pairingPairing-Session aktiv
pairedgekoppelt; Wartefrist auf die erste WS-Steuersitzung aktiv
connectedgekoppelte WS-Steuersitzung aktiv
offlinegekoppelt; WLAN oder bestätigte WS-Steuersitzung nicht verfügbar
recovery_portalphysisch ausgelöster Recovery-Modus
updatingOTA-Transaktion von preparing bis einschließlich restarting aktiv
errornicht behebbarer Laufzeitfehler

Zustandsübergänge ohne OTA:

  • Nach Boot oder erfolgreichem Pairing wartet das Gerät im Zustand paired

höchstens 45000 ms auf session.ready.

  • Wird innerhalb dieser Frist keine Steuersitzung aktiv, wechselt es zu offline.
  • Verlust oder Heartbeat-Timeout einer zuvor aktiven Steuersitzung bewirkt

unmittelbar offline.

  • Eine erfolgreiche neue session.ready-Antwort bewirkt connected.
  • Bei nicht erreichbarem WLAN gilt unmittelbar offline, außer ein Setup- oder

Recovery-Portal ist aktiv; diese Zustände haben Vorrang.

6.2 Pairing-Zustand

Discovery, /device und /pairing/status verwenden:

ZustandBedeutung
pairablekeine vollständige persistente Kopplung
pairinggültige unbestätigte Pairing-Session
pairedvollständige persistente Kopplung

Der Pairing-Zustand ist unabhängig vom aktuellen WLAN- oder WebSocket-Zustand.

7. Discovery über mDNS/DNS-SD

7.1 Service

Das Gerät MUSS folgenden Dienst publizieren:

_homeess-hdp._tcp.local

Die Veröffentlichungspflicht gilt, sobald das Gerät eine aktive IPv4-Stationsverbindung besitzt. Im ausschließlich aktiven Setup- oder Recovery-AP ist mDNS nicht vorgeschrieben.

Der SRV-Port ist der HTTP-API-Port. Der Hostname SOLL homeess-hdp-<letzte-6-Zeichen-der-device_id>.local lauten.

7.2 TXT-Records

KeyFormat
device_idAbschnitt 4.1
protocol_versionexakt 1.0-draft
runtime_profileaktives Profil: pixel-timeline-v1 oder binary-io-v1
firmware_versionSemVer
platformnicht leerer ASCII-Identifier
pairing_statepairable, pairing, paired
binding_id64 lowercase Hexzeichen oder leer
configured_device_typegültiger device_type nach Abschnitt 12.2 oder leer
hardware_config_presenttrue oder false
config_revisiondezimaler uint32
api_portdezimaler Port
ws_portdezimaler Port
ota_portdezimaler Port

TXT-Records sind Discovery-Hinweise. Vor jeder zustandsändernden Aktion MUSS der Adapter /device oder /pairing/status als maßgebliche Quelle abfragen.

Nach Pairing, Entkopplung oder Konfigurationsänderung MUSS das Gerät seine TXT-Records innerhalb von 2 Sekunden aktualisieren.

Der Adapter SOLL kontinuierlich browsen und gleiche device_id unabhängig von IP-Adressänderungen als dasselbe Gerät behandeln.

8. Authentifizierung

8.1 Profile

HDP 1.0-draft definiert genau ein Owner-Authentifizierungsprofil:

local-binding-key-v1

HTTP-Requests mit Authentifizierungsklasse A MÜSSEN enthalten:

X-HDP-Instance: <instance_id>
X-HDP-Binding-Key: <binding_key>

Beide Werte MÜSSEN vorhanden sein. Teilweise vorhandene Credentials ergeben INCOMPLETE_BINDING_CREDENTIALS. Falsche oder fremde Credentials ergeben AUTH_REQUIRED. Binding-Key-Vergleiche SOLLEN, soweit die Plattform dies unterstützt, in konstanter Zeit erfolgen.

8.2 Autorisierungsmatrix

Endpoint/KanalUngekoppeltGekoppelt
DiscoveryPP
GET /devicePP
GET /manifestPP
GET /statusPP
POST /pairing/startPabgelehnt
POST /pairing/confirmPairing-Sessionidempotent nur bei identischem Binding
GET /pairing/statusPP; Credentials optional zum Abgleich
GET /configPA
PUT /configPA
POST /unpairabgelehntA
POST /restartP im AP/Recovery-ModusA
POST /factory-resetnur Recovery-Modusnur Recovery-Modus
GET /firmwarePP
GET /firmware/statusPP
POST /firmware/updateabgelehntA
POST /firmware/restartabgelehntA
WebSocketabgelehntA

P bedeutet öffentlich im lokalen Netz, A bedeutet Owner-authentifiziert.

9. HTTP-Endpunktübersicht

Alle Pfade in dieser Tabelle liegen unter /api/v1.

MethodePfadErfolg
GET/device200
GET/manifest200
GET/status200
POST/pairing/start201
POST/pairing/confirm200
GET/pairing/status200
GET/config200
PUT/config200
POST/unpair202
POST/restart202
POST/factory-reset202
GET/firmware200
GET/firmware/status200
POST/firmware/update202
POST/firmware/restart202

10. Identität, Manifest und Status

10.1 GET /api/v1/device

Request-Body: keiner.

Response:

{
  "ok": true,
  "data": {
    "device_id": "hdp-esp8266-a1b2c3",
    "model": "HDP Universal ESP8266",
    "platform": "esp8266",
    "firmware_version": "0.2.0",
    "protocol_version": "1.0-draft",
    "runtime_profile": "pixel-timeline-v1",
    "pairing_state": "paired",
    "paired": true,
    "binding_id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "configured_device_type": "percentage_indicator",
    "hardware_config_present": true,
    "hardware_config_revision": 4
  }
}

Invarianten:

  • paired ist genau bei pairing_state == "paired" wahr.
  • runtime_profile entspricht exakt dem durch configured_device_type

ausgewählten Profil.

  • binding_id ist nur bei paired=true ein String, sonst null.
  • configured_device_type ist ohne Hardwarekonfiguration null.
  • hardware_config_revision ist ohne Hardwarekonfiguration 0.

10.2 GET /api/v1/manifest

Request-Body: keiner.

Response:

{
  "ok": true,
  "data": {
    "protocol_version": "1.0-draft",
    "api_version": "v1",
    "auth_profile": "local-binding-key-v1",
    "runtime_profile": "pixel-timeline-v1",
    "device_type_profile": "boot-dispatch-v1",
    "device_types": ["percentage_indicator", "argb_output", "binary_io"],
    "output_types": ["argb_strip"],
    "frame_encodings": [
      "rgb8-base64",
      "pixel-list-v1"
    ],
    "timeline_encodings": [
      "hdtl-delta-v1"
    ],
    "features": {
      "mdns": true,
      "websocket": true,
      "ota": true,
      "frame_output": true,
      "timeline_output": true,
      "timeline_loop": true,
      "runtime_brightness": true,
      "binary_input": true,
      "binary_output": true
    },
    "hardware_capabilities": {
      "argb_pins": [0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16],
      "led_types": ["WS2812"],
      "color_orders": ["RGB", "GRB"],
      "binary_pins": [0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16],
      "binary_pullup_pins": [0, 1, 2, 3, 4, 5, 12, 13, 14],
      "binary_boot_sensitive_pins": [0, 2, 15],
      "binary_serial_pins": [1, 3],
      "binary_input_types": ["switch", "button"]
    },
    "limits": {
      "maximum_json_body_bytes": 3072,
      "maximum_websocket_message_bytes": 2048,
      "maximum_outputs": 1,
      "maximum_binary_pins": 11,
      "binary_input_debounce_milliseconds": 30,
      "maximum_led_count": 300,
      "minimum_frame_interval_milliseconds": 20,
      "maximum_timeline_bytes": 65536,
      "maximum_timeline_events": 4096,
      "maximum_timeline_chunk_bytes": 512,
      "maximum_timeline_duration_milliseconds": 86400000
    }
  }
}

hardware_capabilities.argb_pins ist die für dieses konkrete Gerät verbindliche Menge zulässiger GPIO-Nummern. Der Adapter MUSS sie statt einer eigenen Boardtabelle verwenden. Dasselbe gilt für hardware_capabilities.binary_pins bei binary_io. Ein GPIO, der einen argb_strip treiben kann, ist auch als Binary-Ausgang zulässig; beide Mengen dürfen sich daher decken.

Die folgenden drei Listen sind OPTIONAL und beschreiben ausschließlich Eigenheiten einzelner GPIOs. Sie schränken die zulässige Pinauswahl NICHT ein; ein Adapter DARF die betroffenen Pins nicht sperren, SOLL sie beim Einrichten aber kenntlich machen. Jede vorhandene Liste MUSS eine duplikatfreie Teilmenge von binary_pins sein.

FeldBedeutung
binary_pullup_pinsGPIOs mit nutzbarem internem Pull-up. Ein GPIO aus binary_pins, der hier fehlt, benötigt als aktiv-low-Eingang einen externen Pull-up; die Firmware konfiguriert ihn dann als INPUT statt INPUT_PULLUP.
binary_boot_sensitive_pinsGPIOs, deren Pegel beim Reset den Bootmodus bestimmt. Ein bereits geschlossener Schalter kann den Start blockieren.
binary_serial_pinsGPIOs, die die serielle Konsole belegen.

Fehlt eine Liste, macht das Gerät zu dieser Eigenschaft keine Aussage; der Adapter MUSS ihr Fehlen als „unbekannt" und nicht als „trifft auf keinen Pin zu" behandeln.

runtime_profile MUSS das Laufzeitprofil des aktiven Gerätetyps sein. device_type_profile=boot-dispatch-v1 bedeutet, dass die Firmware zwischen den in device_types genannten Laufzeitschichten umschaltet. Das Manifest beschreibt die Fähigkeiten aller Schichten, auch wenn immer nur eine davon aktiv ist.

Gerätetypen mit demselben Laufzeitprofil teilen sich Hardwarevertrag und Nachrichtensatz vollständig; sie unterscheiden sich ausschließlich in der Bedeutung, die der Adapter der Ausgabe gibt. percentage_indicator und argb_output sind ein solches Paar: Beide beschreiben genau einen argb_strip und tauschen Frames und Timelines über pixel-timeline-v1 aus. Die Prozentanzeige füllt den Strang anteilig; der ARGB-Ausgang verknüpft einzelne LEDs mit einzelnen Zuständen. Welche Pixel gesetzt werden, berechnet in beiden Fällen ausschließlich der Adapter — das Gerät kennt weder Prozentwert noch Zustandsquelle.

output_types, frame_encodings und timeline_encodings enthalten ausschließlich die von diesem konkreten Gerät ausführbaren Profile. Alle limits sind verbindliche Obergrenzen beziehungsweise beim Frameintervall eine Untergrenze. Der Adapter MUSS diese Werte abfragen und DARF keine fest einprogrammierte Board- oder Kapazitätstabelle an ihre Stelle setzen.

maximum_timeline_bytes bezeichnet die dekodierte Binärgröße eines Programms, nicht die Länge seiner Base64-Darstellung. maximum_timeline_chunk_bytes bezeichnet entsprechend die dekodierte Nutzlast eines Chunks.

10.3 GET /api/v1/status

Request-Body: keiner.

Response:

{
  "ok": true,
  "data": {
    "state": "paired",
    "uptime_seconds": 123,
    "free_heap_bytes": 38240,
    "wifi_connected": true,
    "wifi_rssi_dbm": -57,
    "ip_address": "192.168.1.42",
    "paired": true,
    "last_boot": {
      "reset_reason": "software_restart",
      "reset_detail": "Software/System restart",
      "config_load_status": "ok",
      "config_load_source": "primary",
      "config_load_diagnostic": "primary=valid; temporary=missing; backup=valid",
      "storage_generation": 12
    }
  }
}
  • wifi_rssi_dbm ist bei getrennter WLAN-Verbindung null.
  • ip_address ist bei getrennter WLAN-Verbindung null.
  • uptime_seconds ist ein uint32 mit den seit dem letzten Boot vergangenen

Sekunden. Ein Adapter DARF einen Rücklauf nur als Hinweis auf Neustart oder Plattformtimer-Wrap verwenden.

  • free_heap_bytes ist ein uint32; wifi_rssi_dbm ist int32|null;

ip_address ist string|null; wifi_connected und paired sind Boolean.

last_boot.reset_reason ist genau einer dieser Werte:

power_on
external_reset
software_restart
watchdog
exception
brownout
deep_sleep
unknown

last_boot.reset_detail ist ein plattformspezifischer Diagnosestring oder null und DARF nicht maschinell ausgewertet werden.

last_boot.config_load_status ist:

WertBedeutung
okprimäre persistente Konfiguration gültig geladen
recoveredgültige staged/Backup-Konfiguration geladen
uninitializedbeim Boot keine persistente Konfiguration vorhanden
invalidpersistente Kandidaten vorhanden, aber keiner vollständig gültig
storage_unavailablepersistenter Speicher beim Boot nicht verfügbar

last_boot.config_load_source ist primary, temporary, backup, defaults oder null. last_boot.config_load_diagnostic ist ein Diagnose-String oder null und DARF nicht maschinell ausgewertet werden. last_boot.storage_generation ist der aktuell im RAM wirksame persistente Generationszähler als uint32.

Bei config_load_status == "invalid" oder "storage_unavailable" DARF das Gerät vorhandene persistente Dateien nicht mit Defaults überschreiben. Der Adapter MUSS den Zustand als Recovery-/Servicefall anzeigen und DARF keine automatische Konfigurationsschreiboperation auslösen. PUT /config, POST /pairing/start und POST /pairing/confirm ergeben in diesem Zustand CONFIG_RECOVERY_REQUIRED, bis die Daten außerhalb des normalen HDP-Ablaufs wiederhergestellt oder per bestätigtem Factory Reset gelöscht wurden.

11. Pairing und Binding

11.1 Verbindlicher Ablauf

Der Adapter MUSS:

  1. einen Datensatz mit device_id, instance_id, adapter_nonce und

binding_key im lokalen Zustand pending dauerhaft speichern;

  1. POST /pairing/start senden;
  2. POST /pairing/confirm senden;
  3. die empfangene binding_id lokal nach Abschnitt 4.5 berechnen und vergleichen;
  4. /pairing/status mit beiden Binding-Headern abfragen;
  5. den lokalen Zustand nur bei binding_status == "match" auf active setzen.

Das Gerät DARF paired erst melden, nachdem Owner-ID und Binding-Key erfolgreich atomar persistiert wurden.

11.2 POST /api/v1/pairing/start

Request:

{
  "instance_id": "homeess-main",
  "protocol_version": "1.0-draft",
  "adapter_nonce": "0123456789abcdef0123456789abcdef"
}

Response HTTP 201:

{
  "ok": true,
  "data": {
    "pairing_session": "11111111111111111111111111111111",
    "adapter_nonce": "0123456789abcdef0123456789abcdef",
    "device_nonce": "22222222222222222222222222222222",
    "expires_in_ms": 120000,
    "security_profile": "local-binding-key-v1"
  }
}

Regeln:

  • Die Session ist 120000 ms ab Erstellung gültig.
  • Die noch unbestätigte Session ist flüchtig und MUSS bei jedem Geräteneustart

ungültig werden. Ein Confirm für eine dadurch verlorene Session ergibt PAIRING_SESSION_EXPIRED, solange noch kein identisches Binding persistent aktiv ist.

  • Sie ist an instance_id und adapter_nonce gebunden.
  • Wiederholung mit identischer instance_id und identischer adapter_nonce

innerhalb der Laufzeit MUSS dieselbe Session und denselben device_nonce zurückgeben; expires_in_ms enthält dabei die verbleibende Laufzeit.

  • Eine andere Startanforderung während einer aktiven Session ergibt

PAIRING_IN_PROGRESS.

  • Ein bereits gekoppeltes Gerät ergibt ALREADY_PAIRED; error.details MUSS die

aktuelle öffentliche binding_id enthalten.

11.3 POST /api/v1/pairing/confirm

Request:

{
  "pairing_session": "11111111111111111111111111111111",
  "instance_id": "homeess-main",
  "adapter_nonce": "0123456789abcdef0123456789abcdef",
  "device_nonce": "22222222222222222222222222222222",
  "binding_key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

Response HTTP 200:

{
  "ok": true,
  "data": {
    "paired": true,
    "device_id": "hdp-esp8266-a1b2c3",
    "instance_id": "homeess-main",
    "binding_id": "e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e"
  }
}

Regeln:

  • Alle vier Sessionwerte MÜSSEN exakt mit /pairing/start übereinstimmen.
  • Die Persistierung von Owner und Key MUSS vor der Erfolgsantwort abgeschlossen

sein.

  • Danach MUSS die Session ungültig werden.
  • Geht die Erfolgsantwort verloren, MUSS eine Wiederholung mit denselben Werten

idempotent dieselbe Erfolgsantwort liefern.

  • Eine Wiederholung mit abweichender Instanz oder abweichendem Key ergibt

ALREADY_PAIRED.

  • Ist dasselbe Binding bereits persistent aktiv, MÜSSEN abweichende oder nicht mehr

bekannte Session- und Nonce-Werte ignoriert werden, sofern instance_id und binding_key exakt passen. Dies ermöglicht die Wiederaufnahme nach einem Geräteneustart zwischen Persistierung und empfangener Erfolgsantwort. Alle Felder MÜSSEN trotzdem ihre in Abschnitt 4 definierte Syntax besitzen.

  • Nach PAIRING_SESSION_EXPIRED MUSS der Adapter zuerst /pairing/status mit

seinem persistenten Pending-Binding abfragen. Bei match aktiviert er dieses lokal; nur bei unpaired beginnt er mit neu erzeugtem adapter_nonce und neuer Session erneut. Bei conflict bricht er mit Eigentümerkonflikt ab.

11.4 GET /api/v1/pairing/status

Request-Body: keiner.

Die Header X-HDP-Instance und X-HDP-Binding-Key sind entweder beide vorhanden oder beide abwesend. Nur einer der Header ergibt INCOMPLETE_BINDING_CREDENTIALS.

Response ohne Credentials bei gekoppeltem Gerät:

{
  "ok": true,
  "data": {
    "pairing_state": "paired",
    "paired": true,
    "binding_id": "e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e",
    "binding_status": "not_checked",
    "paired_to_requester": null
  }
}

Mit Credentials ist binding_status:

GerätezustandCredentialsbinding_statuspaired_to_requester
ungekoppeltkeineunpairednull
ungekoppeltvollständigunpairedfalse
gekoppeltkeinenot_checkednull
gekoppeltOwner und Key korrektmatchtrue
gekoppeltOwner oder Key falschconflictfalse

11.5 Verbindliche Wiederherstellungsmatrix

AdapterzustandGerätestatusAdapteraktion
kein DatensatzunpairedPairing darf angeboten werden
kein Datensatzpaired/not_checkedals fremd anzeigen; nicht automatisch übernehmen
pendingunpairedPairing mit neuer Session fortsetzen
pendingmatchlokalen Datensatz auf active setzen
pendingconflictlokalen Pending-Datensatz verwerfen; Konflikt anzeigen
activematchnormal verbinden
activeunpairedlokalen Datensatz deaktivieren; neu koppeln anbieten
activeconflictlokalen Datensatz deaktivieren; Eigentümerkonflikt anzeigen

Kein Konfliktfall DARF durch Überschreiben des Geräte-Bindings behoben werden.

12. Hardwarekonfiguration

12.1 Konfigurationsobjekt

{
  "revision": 4,
  "device_type": "percentage_indicator",
  "outputs": [
    {
      "output_id": "main",
      "output_type": "argb_strip",
      "pin": 4,
      "pixel_count": 10,
      "driver": "WS2812",
      "color_order": "GRB",
      "reverse": false,
      "maximum_brightness_percent": 35,
      "maximum_current_milliamps": 500,
      "current_per_pixel_milliamps": 60,
      "offline_mode": "retain_last_frame"
    }
  ]
}

Für binary_io lautet dasselbe vollständige Konfigurationsobjekt alternativ:

{
  "revision": 7,
  "device_type": "binary_io",
  "pins": [
    { "pin": 4, "direction": "input", "input_type": "switch" },
    { "pin": 5, "direction": "input", "input_type": "button" },
    { "pin": 12, "direction": "output" }
  ]
}

Für argb_output enthält dasselbe Objekt beide Abschnitte: outputs beschreibt den Strang, pins die Binary-Rollen aller übrigen GPIOs.

{
  "revision": 11,
  "device_type": "argb_output",
  "outputs": [ { "output_id": "main", "pin": 4, "…": "…" } ],
  "pins": [
    { "pin": 0, "direction": "input", "input_type": "button" },
    { "pin": 1, "direction": "input", "input_type": "switch" },
    { "pin": 15, "direction": "output" },
    { "pin": 16, "direction": "output" }
  ]
}

Für percentage_indicator steht ausschließlich outputs, für binary_io ausschließlich pins.

12.2 Feldregeln

FeldTypErlaubte Werte
revisionuint32nur Response
device_typestringEintrag aus manifest.device_types
outputsarray1…limits.maximum_outputs vollständige Objekte
outputs[].output_idstring1…32 Zeichen aus A-Z, a-z, 0-9, ., _, -; innerhalb der Konfiguration eindeutig
outputs[].output_typestringEintrag aus manifest.output_types
outputs[].pinuint8Eintrag aus hardware_capabilities.argb_pins
outputs[].pixel_countuint161…limits.maximum_led_count
outputs[].driverstringEintrag aus hardware_capabilities.led_types
outputs[].color_orderstringEintrag aus hardware_capabilities.color_orders
outputs[].reversebooleantrue, false
outputs[].maximum_brightness_percentuint80…100
outputs[].maximum_current_milliampsuint161…20000
outputs[].current_per_pixel_milliampsuint81…100
outputs[].offline_modestringretain_last_frame, clear, continue_timeline

Für binary_io gelten zusätzlich beziehungsweise anstelle von outputs:

FeldTypErlaubte Werte
pinsarray1…limits.maximum_binary_pins vollständige Objekte
pins[].pinuint8eindeutiger Eintrag aus hardware_capabilities.binary_pins
pins[].directionstringinput, output
pins[].input_typestringbei input Pflicht: switch, button; bei output verboten

12.2.1 Feste GPIO-Belegung bei argb_output

Bei binary_io ist die Pinbelegung frei. Bei argb_output ist sie es NICHT: Sie folgt zwingend der Hardware, damit am Gerät außer dem Datenpin nichts zu entscheiden bleibt. Es gilt genau eine Regel, abgeleitet allein aus hardware_capabilities.binary_pins und binary_pullup_pins:

GPIORolle
in binary_pins, nicht in binary_pullup_pinsfest output — ohne nutzbaren internen Pull-up taugt er nicht als aktiv-low-Eingang, wohl aber zum Schalten von Relais oder Schützen
outputs[0].pinARGB-Datenleitung; MUSS in binary_pullup_pins enthalten sein und DARF in pins nicht vorkommen
alle übrigen aus binary_pinsinput mit frei wählbarem input_type

pins MUSS deshalb jeden Eintrag aus binary_pins außer dem Datenpin genau einmal in der vorgeschriebenen Richtung enthalten. Eine unvollständige Liste, eine abweichende Richtung oder ein Datenpin ohne Pull-up MÜSSEN mit INVALID_CONFIGURATION abgelehnt werden — eine Lücke wäre eine stille Fehlbelegung. Der Adapter leitet die Liste ab, statt sie erfragen zu lassen; das Gerät prüft sie gegen dieselbe Regel.

Ein argb_output-Gerät verarbeitet damit sowohl die output.*- als auch die binary.*-Nachrichten seines Laufzeitprofils. Welche Nachrichten ein Gerät annimmt, folgt dem konfigurierten device_type; welche es überhaupt beherrscht, steht in features.

device_type wählt ausschließlich die generische Laufzeitschicht. Jede darüber hinausgehende Semantik bleibt beim Adapter. Ein Gerätetypwechsel MUSS persistent gespeichert werden. Wechselt dabei auch das Laufzeitprofil, MUSS das Gerät den neuen Typ durch einen Neustart aktivieren; die bisher aktive Laufzeitschicht DARF nicht im laufenden Betrieb mit der neuen Pinbelegung vermischt werden. Bleibt das Laufzeitprofil gleich — etwa zwischen percentage_indicator und argb_output —, gilt der Wechsel wie jede andere Konfigurationsänderung: Er wird sofort übernommen und über config.changed bekanntgegeben, ein Neustart wäre hier eine grundlose Unterbrechung.

HDP 1.0-draft definiert für output_type=argb_strip ausschließlich driver=WS2812. Pixelindizes auf dem Wire sind immer logische Indizes 0…pixel_count-1. Bei reverse=false entspricht der logische Index dem physischen Index; bei reverse=true gilt:

physischer_index = pixel_count - 1 - logischer_index

Offline-Verhalten:

WertVerbindliche Wirkung beim Übergang zu offline
retain_last_frameaktive Timeline stoppen und den zuletzt physisch ausgegebenen Frame unverändert halten
clearaktive Timeline stoppen und alle Pixel unverzüglich auf 0,0,0 setzen
continue_timelineeine bereits laufende, als Loop gestartete Timeline ohne Adapterverbindung lokal fortsetzen; sonst letzten Frame halten

Bei Neustart sind Pixelpuffer und Timelines verloren; jeder Ausgang startet schwarz. Nach session.ready MUSS der Adapter den gewünschten absoluten Frame ersetzen oder eine vollständig hochgeladene Timeline starten. Eine anwendungsspezifische Offline-Anzeige kann ausschließlich als zuvor gestartete Loop-Timeline in Verbindung mit continue_timeline realisiert werden.

Binary-Eingänge verwenden INPUT_PULLUP; logisch state=true bedeutet deshalb einen elektrisch nach GND gezogenen, aktiven Eingang. Das Gerät MUSS sowohl Flanken zum aktiven als auch zum inaktiven Zustand 30 ms entprellen. Schalter erzeugen nach stabiler Änderung ein Ereignis; Taster erzeugen nur beim stabilen Übergang auf aktiv ein pressed-Ereignis. Binary-Ausgänge MÜSSEN beim Booten, bei Controllerverlust und während eines Gerätetypwechsels logisch false/LOW sein. Invertierung gehört ausschließlich in das Adapter-Binding.

12.3 GET /api/v1/config

Response:

{
  "ok": true,
  "data": {
    "revision": 4,
    "device_type": "percentage_indicator",
    "outputs": [
      {
        "output_id": "main",
        "output_type": "argb_strip",
        "pin": 4,
        "pixel_count": 10,
        "driver": "WS2812",
        "color_order": "GRB",
        "reverse": false,
        "maximum_brightness_percent": 35,
        "maximum_current_milliamps": 500,
        "current_per_pixel_milliamps": 60,
        "offline_mode": "retain_last_frame"
      }
    ]
  }
}

Revision 0 bedeutet, dass noch keine explizite Hardwarekonfiguration gespeichert wurde. GET /config liefert auch dann das vollständige wirksame Defaultobjekt. Das erste Schreiben verwendet expected_revision: 0 und erzeugt Revision 1.

12.4 PUT /api/v1/config

Request:

{
  "expected_revision": 4,
  "config": {
    "device_type": "percentage_indicator",
    "outputs": [
      {
        "output_id": "main",
        "output_type": "argb_strip",
        "pin": 4,
        "pixel_count": 10,
        "driver": "WS2812",
        "color_order": "GRB",
        "reverse": false,
        "maximum_brightness_percent": 35,
        "maximum_current_milliamps": 500,
        "current_per_pixel_milliamps": 60,
        "offline_mode": "retain_last_frame"
      }
    ]
  }
}

Regeln:

  • expected_revision und config sind Pflichtfelder.
  • expected_revision MUSS der aktuell gespeicherten Revision entsprechen.
  • Der Request MUSS eine vollständige Konfiguration enthalten; partielle Updates

sind nicht erlaubt.

  • Validierung und Hardwareinitialisierung MÜSSEN vor der Persistierung erfolgreich

sein.

  • Der zu speichernde Kandidat MUSS exakt die aktuelle Revision plus 1 enthalten.
  • Konfiguration und neue Revision MÜSSEN in einer atomaren Transaktion gespeichert

werden.

  • Erst nach erfolgreicher Persistierung dürfen neue Revision und Inhalt nach außen

sichtbar werden und die Erfolgsantwort gesendet werden.

  • Bleibt device_type unverändert, MUSS nach Erfolg config.changed per

WebSocket gesendet und mDNS aktualisiert werden.

  • Ändert sich device_type, MUSS das Gerät erst die Erfolgsantwort vollständig

senden und 500…2000 ms danach neu starten. Die bestehende WebSocket-Sitzung endet dabei; das neue runtime_profile wird erst nach dem Boot per mDNS, /device, /manifest und neuem Hello sichtbar.

  • Nach erfolgreicher Pixel-Konfigurationsänderung MUSS das Gerät aktive Timelinewiedergaben stoppen,

alle hochgeladenen Timelines verwerfen und sämtliche neu konfigurierten Ausgänge schwarz initialisieren. Ein fehlgeschlagener PUT DARF den bisherigen Frame, die bisherige Konfiguration oder das Binding nicht verändern.

Bei Revisionskonflikt:

{
  "ok": false,
  "error": {
    "code": "CONFIG_REVISION_CONFLICT",
    "message": "Configuration revision does not match.",
    "details": {
      "expected_revision": 4,
      "current_revision": 5
    }
  }
}

12.5 Verlorene Konfigurationsantwort

Nach Timeout eines PUT /config DARF der Adapter nicht blind erneut schreiben. Er MUSS GET /config ausführen:

  • Revision um 1 erhöht und Inhalt identisch: ursprünglicher Request war erfolgreich.
  • Revision unverändert: Request darf mit gleichem expected_revision wiederholt werden.
  • Andere Revision oder anderer Inhalt: Konflikt anzeigen und Benutzerentscheidung

verlangen.

12.6 Verbindliche RGB- und Schutzregeln

Alle Frame- und Timelineformate verwenden unabhängig von color_order exakt drei Nutzbytes pro Pixel in der Reihenfolge Rot, Grün, Blau. Das Gerät setzt erst im Hardwaretreiber auf die physische Byteanordnung um. Jeder Nutzwert ist ein uint8 von 0 bis 255; es gibt keinen impliziten Alpha-, Prozent-, Gamma- oder Helligkeitskanal.

Der Adapter berechnet sämtliche semantischen Helligkeiten bereits in diese RGB-Nutzwerte ein. Das Gerät wendet anschließend ausschließlich die hardwarebezogenen Schutzgrenzen an. Für einen Frame mit N Pixeln gilt:

helligkeitsfaktor = maximum_brightness_percent / 100

rohstrom_mA =
  summe(i = 0 … N-1,
        current_per_pixel_milliamps
        × (r_i + g_i + b_i) / (3 × 255)
        × helligkeitsfaktor)

Liegt rohstrom_mA über maximum_current_milliamps, MUSS das Gerät alle RGB-Komponenten des Frames zusätzlich mit demselben Faktor

maximum_current_milliamps / rohstrom_mA

skalieren. Nach jeder Multiplikation wird auf den nächsten uint8 gerundet und auf 0…255 begrenzt. Dadurch bleiben Farben proportional. Frame- und Timelineoperationen arbeiten immer auf den unskalierten logischen RGB-Werten; die Schutzskalierung wird erst für die physische Ausgabe berechnet und verändert den logischen Pixelpuffer nicht.

Das Gerät DARF keine weitere anwendungsspezifische Interpolation hinzufügen. Zeitliche Zwischenwerte müssen vom Adapter als Frame oder Timelineereignis vollständig vorgegeben werden.

12.8 Sensorkonfiguration

Der Gerätetyp sensors enthält ein bis acht Einträge in sensors und darf zusätzlich freie GPIOs in pins als Binary-I/O konfigurieren. Sensorbusse und Binary-Pins DÜRFEN sich nicht überschneiden. Alle I²C-Sensoren MÜSSEN dasselbe SDA/SCL-Paar verwenden und auf diesem Bus eindeutige Adressen besitzen.

{
  "revision": 7,
  "device_type": "sensors",
  "sensors": [
    {
      "sensor_id": "climate",
      "sensor_type": "bme280",
      "sample_interval_milliseconds": 5000,
      "sda_pin": 4,
      "scl_pin": 5,
      "address": 118
    },
    {
      "sensor_id": "weight",
      "sensor_type": "hx711",
      "sample_interval_milliseconds": 500,
      "data_pin": 12,
      "clock_pin": 13
    }
  ],
  "pins": [
    { "pin": 14, "direction": "input", "input_type": "button" }
  ]
}

Unterstützte sensor_type-Werte sind dht11, dht22, ds18b20, bme280, sht30, sht31, bh1750, ina219, hx711, vl53l0x und analog. Ein-Pin-Sensoren verwenden pin, HX711 verwendet data_pin und clock_pin, I²C-Sensoren verwenden sda_pin, scl_pin und address. analog belegt den separaten ADC A0 und besitzt deshalb kein GPIO-Feld. Das D1-mini-Manifest meldet für A0 eine Auflösung von 10 Bit und wegen des Board-Spannungsteilers einen maximalen Eingang von 3200 mV; die Firmware überträgt dennoch bewusst nur den unskalierten Wert 0 bis 1023.

12.9 R503-Fingerabdruckleser

fingerprint_reader verbindet genau einen R503 über 3,3-V-TTL-UART mit fest 57600 Baud. rx_pin bezeichnet den Empfangspin des ESP8266 und wird mit TX des R503 verbunden; tx_pin entsprechend mit RX. wakeup_pin ist optional und bei nicht angeschlossener Wakeup-Leitung null. UART, Wakeup und freie Binary-Pins dürfen sich nicht überschneiden; GPIO 16 ist als UART-RX unzulässig. Die Standardbelegung RX GPIO 13 / TX GPIO 15 nutzt den umgelegten nativen ESP8266-UART und ist wegen des fehlenden Interruptpuffers zu bevorzugen. Andere zulässige Pinpaare bleiben über den begrenzt gepufferten Software-UART verfügbar. Während der native UART dem R503 zugeordnet ist, bleibt die serielle Diagnoseausgabe deaktiviert und darf keine Bytes in das Sensorprotokoll senden. Lernen und Löschen sind erst zulässig, nachdem fingerprint.status sowohl online: true als auch eine erfolgreich gelesene Kapazität gemeldet hat. Bei online: false bleibt die Bedienung gesperrt und der Adapter MUSS last_error sichtbar machen. Insbesondere bedeutet uart_timeout, dass auf der konfigurierten Kreuzverbindung R503-TX nach ESP-RX und R503-RX nach ESP-TX keine gültige Antwort eingegangen ist.

{
  "revision": 8,
  "device_type": "fingerprint_reader",
  "uart": { "rx_pin": 13, "tx_pin": 15 },
  "wakeup_pin": 12,
  "led": {
    "idle": { "effect": "breathing", "color": "blue", "speed": 96, "count": 0 },
    "scanning": { "effect": "on", "color": "blue", "speed": 0, "count": 0 },
    "success": { "effect": "flashing", "color": "blue", "speed": 32, "count": 2 },
    "failure": { "effect": "flashing", "color": "red", "speed": 32, "count": 2 },
    "enrolling": { "effect": "breathing", "color": "purple", "speed": 64, "count": 0 }
  },
  "pins": [{ "pin": 4, "direction": "input", "input_type": "button" }]
}

Die fünf LED-Szenen sind Pflicht. Zulässige Effekte sind breathing, flashing, on, off, gradual_on und gradual_off; Farben sind red, blue und purple. speed und count sind uint8. Vorlagen und biometrische Merkmale verbleiben im R503. Das Gerät überträgt keine Fingerbilder oder Merkmalsdaten und wertet keine homeESS-State-Aktion aus.

13. Entkopplung, Neustart und Factory Reset

13.1 POST /api/v1/unpair

Authentifizierung: A.

Request:

{
  "preserve_hardware_config": true
}

Nur true ist in HDP 1.0-draft zulässig.

Response HTTP 202:

{
  "ok": true,
  "data": {
    "hardware_config_retained": true,
    "restart_required": true
  }
}

Vor der Response MUSS das Gerät Owner-ID, Binding-Key und WLAN-Zugangsdaten atomar löschen. Hardwarekonfiguration und device_id bleiben erhalten. Das Gerät startet 500…2000 ms nach der Response neu. Eine aktive WebSocket-Sitzung MUSS unmittelbar nach erfolgreicher Persistierung geschlossen werden; über das alte Binding dürfen keine weiteren Laufzeitwerte angenommen werden.

13.2 POST /api/v1/restart

Request:

{}

Response HTTP 202:

{
  "ok": true,
  "data": {
    "restart_required": true
  }
}

Das Gerät startet 500…2000 ms nach der Response neu. Der Adapter DARF diesen Endpoint nach einem Response-Timeout nicht automatisch wiederholen.

13.3 POST /api/v1/factory-reset

Dieser Endpoint ist ausschließlich im Betriebszustand recovery_portal zulässig.

Request:

{
  "confirmation": "hdp-esp8266-a1b2c3"
}

confirmation MUSS exakt der aktuellen device_id entsprechen.

Response HTTP 202:

{
  "ok": true,
  "data": {
    "factory_reset": true,
    "restart_required": true
  }
}

Das Gerät MUSS sämtliche persistenten Werte löschen und nach 500…2000 ms neu starten. Aktive WebSocket-Sitzungen und das alte Binding werden unmittelbar nach erfolgreichem Löschen unwirksam. Außerhalb des Recovery-Modus ergibt der Endpoint FACTORY_RESET_NOT_ALLOWED.

14. WebSocket-Protokoll

14.1 Verbindung

  • URI: ws://<device-ip>:<ws_port>/api/v1/ws
  • RFC-6455-Version: 13
  • Authentifizierung: HTTP Basic
  • Benutzername: instance_id
  • Passwort: binding_key
  • Base64-Eingabe: UTF-8-Bytes von <instance_id>:<binding_key>
  • maximal eine aktive Steuersitzung pro Gerät

Der Request-Header lautet exakt:

Authorization: Basic <base64-token>

<base64-token> verwendet das Standardalphabet aus RFC 4648 einschließlich erforderlicher =-Paddingzeichen. Innerhalb des Tokens sind CR, LF, Leerzeichen und Zeilenfaltung unzulässig. Insbesondere darf ein Encoder lange HDP-Credentials nicht nach 72 Zeichen umbrechen.

Eine neue gültig authentifizierte Steuersitzung ersetzt eine bestehende Sitzung. Die alte Sitzung erhält SESSION_REPLACED und wird danach geschlossen.

Ungültige Credentials MÜSSEN den HTTP-Upgrade mit 401 ablehnen. Die 401-Antwort MUSS syntaktisch gültiges HTTP/1.1 sein, den Header WWW-Authenticate: Basic realm="homeESS HDP" enthalten und ihre Headersektion mit \r\n\r\n abschließen. Falls ein Body gesendet wird, MUSS Content-Length dessen exakter Bytezahl entsprechen. Eine bodylose Antwort verwendet Content-Length: 0.

14.2 Nachrichten-Envelope

Jede Textnachricht MUSS dieses Envelope besitzen:

{
  "type": "output.frame.set",
  "message_id": "adapter-42",
  "sequence": 42,
  "payload": {}
}
  • Binärframes sind nicht erlaubt.
  • Die maximale Textgröße ist

manifest.limits.maximum_websocket_message_bytes UTF-8-Bytes.

  • Jede HDP-Nachricht MUSS als einzelner, nicht fragmentierter RFC-6455-Textframe

übertragen werden. Fragmentierte Datenframes sind in diesem Profil nicht zulässig und ergeben INVALID_REQUEST; danach wird die Verbindung geschlossen.

  • Binäre Frame- und Timeline-Nutzdaten werden innerhalb des JSON-Envelopes mit

dem Standardalphabet aus RFC 4648 Base64-kodiert. CR, LF, Leerzeichen, URL-safe Alphabet und fehlendes oder überflüssiges Padding sind unzulässig.

  • sequence ist ein uint32.
  • Jede Richtung besitzt eine unabhängige Sequenz.
  • Die erste Sequenz jedes Senders ist 1.
  • Jede weitere Sequenz ist exakt vorherige Sequenz + 1.
  • Ein Sprung, Duplikat oder Rücklauf ergibt SEQUENCE_ERROR; danach wird die

Verbindung geschlossen.

  • Sequenzen beginnen nach jeder neuen WebSocket-Verbindung wieder bei 1.
  • Vor dem Senden einer Sequenz nach 4294967295 MUSS der Sender die Verbindung

schließen und neu aufbauen; ein Wrap auf 0 ist unzulässig.

14.3 Sitzungsaufbau

Unmittelbar nach dem Upgrade sendet das Gerät:

{
  "type": "device.hello",
  "message_id": "device-1",
  "sequence": 1,
  "payload": {
    "device_id": "hdp-esp8266-a1b2c3",
    "protocol_version": "1.0-draft",
    "runtime_profile": "pixel-timeline-v1",
    "config_revision": 4,
    "heartbeat_interval_ms": 15000,
    "heartbeat_timeout_ms": 45000
  }
}

Der Adapter MUSS innerhalb von 5000 ms antworten:

{
  "type": "homeess.hello",
  "message_id": "adapter-1",
  "sequence": 1,
  "payload": {
    "instance_id": "homeess-main",
    "protocol_version": "1.0-draft",
    "runtime_profile": "pixel-timeline-v1"
  }
}

Das Gerät antwortet:

{
  "type": "session.ready",
  "message_id": "device-2",
  "sequence": 2,
  "payload": {
    "config_revision": 4,
    "runtime_profile": "pixel-timeline-v1"
  }
}

Die Beispiele zeigen pixel-timeline-v1. Für ein als binary_io konfiguriertes Gerät steht an allen drei Stellen stattdessen exakt binary-io-v1. Ein Profilmix innerhalb derselben Sitzung ist unzulässig.

Vor session.ready DARF der Adapter keine Laufzeitwerte senden. Fehler oder Timeout schließen die Verbindung. Die Fehlerzuordnung ist:

VerletzungCode
innerhalb 5000 ms keine vollständige NachrichtSESSION_HELLO_TIMEOUT
erste Nachricht ist kein vollständiges homeess.helloINVALID_REQUEST
instance_id stimmt nicht mit dem Binding übereinAUTH_REQUIRED
protocol_version stimmt nichtUNSUPPORTED_PROTOCOL_VERSION
runtime_profile stimmt nichtUNSUPPORTED_RUNTIME_PROFILE

Weicht config_revision in device.hello oder session.ready von der lokal bekannten Revision ab, MUSS der Adapter vor der nächsten mutierenden output.*- oder binary.output.*-Nachricht GET /config ausführen.

14.4 Heartbeat

Der Adapter sendet spätestens alle 15000 ms:

{
  "type": "ping",
  "message_id": "adapter-9",
  "sequence": 9,
  "payload": {}
}

Das Gerät antwortet mit eigenem message_id und eigener Sequenz:

{
  "type": "pong",
  "message_id": "device-10",
  "sequence": 10,
  "payload": {
    "reply_to": "adapter-9"
  }
}

Empfängt das Gerät 45000 ms lang keine gültige Adapter-Nachricht, MUSS es HEARTBEAT_TIMEOUT senden und die Verbindung schließen.

14.4.1 Gerätetelemetrie

Nach session.ready sendet das Gerät frühestens nach 1000 ms und spätestens nach 15000 ms eine aktuelle Telemetrie. Danach sendet es mindestens alle 15000 ms sowie nach einer physischen Ausgabeveränderung, jedoch höchstens einmal pro 1000 ms:

{
  "type": "device.status",
  "message_id": "device-12",
  "sequence": 12,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "wifi_rssi": -57,
    "effective_brightness": 20,
    "runtime_brightness": 100,
    "estimated_current_milliamps": 11.2,
    "power_limit_active": false
  }
}

wifi_rssi ist die aktuelle WLAN-Signalstärke in dBm oder null, wenn keine WLAN-Verbindung besteht. effective_brightness ist ein uint8 von 0 bis 100 und bezeichnet den tatsächlich auf RGB-Komponenten angewendeten Helligkeitsfaktor nach Laufzeithelligkeit, konfiguriertem Helligkeits- und Stromlimit. runtime_brightness spiegelt bei features.runtime_brightness=true den zuletzt angewendeten Laufzeitfaktor von 0 bis 100. estimated_current_milliamps ist eine nichtnegative Zahl und wird aus dem logischen Pixelbild, current_per_pixel_milliamps und den aktiven Limits berechnet; sie ist ausdrücklich keine Messung durch einen Stromsensor. power_limit_active ist genau dann true, wenn das konfigurierte Stromlimit den Ausgang zusätzlich dimmt.

output_id und config_revision bezeichnen den Zustand, auf den sich die Ausgabetelemetrie bezieht. Das Ereignis verändert die physische Ausgabe nicht.

Im Profil binary-io-v1 enthält device.status keine erfundene Helligkeits- oder Stromtelemetrie, sondern ausschließlich:

{
  "type": "device.status",
  "message_id": "device-12",
  "sequence": 12,
  "payload": {
    "config_revision": 7,
    "runtime_profile": "binary-io-v1",
    "wifi_rssi": -57
  }
}

Pinzustände werden verbindlich über Abschnitt 14.9 übertragen.

14.5 Direkte Frames

Direkte Frames sind für Initialzustände und seltene Änderungen vorgesehen. Animationen DÜRFEN NICHT durch netzwerkgetaktete Folgen direkter Frames erzeugt werden; dafür ist Abschnitt 14.6 zu verwenden.

14.5.1 Absoluter Ersatzframe

{
  "type": "output.frame.set",
  "message_id": "adapter-42",
  "sequence": 42,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "frame_id": "frame-17",
    "mode": "replace",
    "encoding": "rgb8-base64",
    "data": "AP8AAP8AAL8AAAAA",
    "pixels": null
  }
}

data dekodiert für vier Pixel zu:

00 ff 00 | 00 ff 00 | 00 bf 00 | 00 00 00

Bei mode=replace gelten:

  • encoding MUSS rgb8-base64 sein;
  • data MUSS ein kanonischer Base64-String sein;
  • pixels MUSS null sein;
  • die dekodierte Länge MUSS exakt pixel_count × 3 Bytes betragen;
  • die Bytes enthalten für logischen Index 0 beginnend jeweils R, G und B.

14.5.2 Sparse Pixeländerung

{
  "type": "output.frame.set",
  "message_id": "adapter-43",
  "sequence": 43,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "frame_id": "frame-18",
    "mode": "patch",
    "encoding": "pixel-list-v1",
    "data": null,
    "pixels": [
      {"index": 1, "r": 0, "g": 255, "b": 0},
      {"index": 2, "r": 0, "g": 191, "b": 0}
    ]
  }
}

Bei mode=patch gelten:

  • encoding MUSS pixel-list-v1 sein;
  • data MUSS null sein;
  • pixels MUSS ein nicht leeres Array sein;
  • jeder Eintrag besitzt ausschließlich die Pflichtfelder index, r, g, b;
  • index ist ein uint16 innerhalb des konfigurierten Ausgangs;
  • r, g, b sind uint8;
  • Indizes MÜSSEN streng aufsteigend und dürfen nicht doppelt sein.

Für beide Modi gelten:

  • output_id MUSS einen aktuell konfigurierten Ausgang bezeichnen;
  • config_revision MUSS exakt der aktiven Revision entsprechen;
  • frame_id folgt der Syntax von message_id;
  • das Gerät MUSS den vollständigen Request validieren, bevor es Pixelpuffer oder

physische Ausgabe verändert;

  • ein akzeptierter Frame stoppt eine auf diesem Ausgang geplante oder laufende

Timeline ohne output.timeline.completed;

  • die Änderung wird atomar auf den logischen Puffer angewendet und verursacht

höchstens eine physische Ausgabe;

  • der Abstand zweier physischer Ausgaben darf

minimum_frame_interval_milliseconds nicht unterschreiten.

Trifft ein direkter Frame vor Ablauf dieses Mindestabstands ein, ergibt er OUTPUT_RATE_LIMITED; weder logischer Puffer noch physische Ausgabe dürfen sich ändern. Timelines werden dagegen nach Abschnitt 14.7 schedulerseitig aufgeholt.

Der erste mutierende Befehl eines Adapters pro Ausgang nach session.ready MUSS mode=replace sein oder eine Timeline starten. Ein Patch ohne solchen Baselinezustand ergibt OUTPUT_BASE_FRAME_REQUIRED. Dadurch hängt das Ergebnis nicht von einem Frame einer früheren oder verlorenen Sitzung ab.

Antwort:

{
  "type": "output.frame.applied",
  "message_id": "device-44",
  "sequence": 44,
  "payload": {
    "reply_to": "adapter-43",
    "output_id": "main",
    "frame_id": "frame-18",
    "config_revision": 4,
    "applied_at_uptime_milliseconds": 182340
  }
}

applied_at_uptime_milliseconds ist der uint32-Wert von der monotonen Gerätezeit beim physischen Schreiben. Ein identisches frame_id mit byteidentischem Payload ist innerhalb derselben Sitzung idempotent und liefert dieselbe Erfolgsantwort. Dasselbe frame_id mit abweichendem Payload ergibt INVALID_OUTPUT_STATE.

14.6 Timeline-Upload

Eine Timeline ist ein vom Adapter vollständig vorberechnetes, semantikfreies Delta-Programm. Das Gerät interpretiert ausschließlich Zeitabstände und Pixeloperationen.

14.6.1 Upload beginnen

{
  "type": "output.timeline.begin",
  "message_id": "adapter-50",
  "sequence": 50,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "timeline_id": "indicator-loop-7",
    "encoding": "hdtl-delta-v1",
    "duration_milliseconds": 10000,
    "event_count": 2,
    "program_size_bytes": 28,
    "program_sha256": "37657360dd397ea89a19031042604b6c6d7816e2f55826dc6ca050e3bd59a6ea"
  }
}

Alle Felder sind Pflichtfelder. timeline_id folgt der Syntax von message_id. Größen, Ereigniszahl und Dauer müssen innerhalb der Manifestlimits liegen; SHA-256 besteht aus exakt 64 lowercase Hexzeichen.

Das Gerät darf pro Sitzung nur einen Staging-Upload halten. Ein laufender oder geplanter Zeitplan auf demselben Ausgang ergibt OUTPUT_BUSY; der Adapter MUSS ihn zuerst stoppen oder durch einen direkten Frame ersetzen. Ein Begin mit identischer Timeline-ID und byteidentischen Metadaten ist idempotent. Ein Begin für eine andere Timeline oder mit abweichenden Metadaten ergibt TIMELINE_UPLOAD_IN_PROGRESS.

Antwort:

{
  "type": "output.timeline.ready",
  "message_id": "device-51",
  "sequence": 51,
  "payload": {
    "reply_to": "adapter-50",
    "output_id": "main",
    "timeline_id": "indicator-loop-7",
    "next_offset": 0,
    "maximum_chunk_bytes": 512
  }
}

14.6.2 Chunks übertragen

{
  "type": "output.timeline.chunk",
  "message_id": "adapter-52",
  "sequence": 52,
  "payload": {
    "timeline_id": "indicator-loop-7",
    "offset": 0,
    "data": "AAAAAAEABAD/ABQAAAACAAEAAACWAAEBAADIAA=="
  }
}

offset ist der uint32-Byteoffset in der dekodierten Programmnutzlast. data MUSS zu 1…maximum_chunk_bytes Bytes dekodieren. Chunks müssen ohne Lücke und in aufsteigender Reihenfolge eintreffen. Das Gerät MUSS Base64 dekodieren, Grenzen prüfen und die Bytes in einen vom aktiven Programm getrennten Stagingbereich schreiben.

Antwort:

{
  "type": "output.timeline.chunk.accepted",
  "message_id": "device-53",
  "sequence": 53,
  "payload": {
    "reply_to": "adapter-52",
    "timeline_id": "indicator-loop-7",
    "next_offset": 28
  }
}

Die exakte Wiederholung des zuletzt akzeptierten Offsets mit identischen dekodierten Bytes ist idempotent und gibt denselben next_offset zurück. Jeder andere Offset ergibt TIMELINE_OFFSET_MISMATCH mit expected_offset in details. Bei WebSocket-Verlust wird ausschließlich der unvollständige Staging-Upload verworfen; Binding, Hardwarekonfiguration, aktiver Frame und bereits committed Timeline bleiben unverändert.

14.6.3 Binärformat hdtl-delta-v1

Alle Mehrbyte-Integer sind unsigned und little-endian. Das Programm ist eine lückenlose Folge von exakt event_count Ereignissen:

BestandteilGrößeBedeutung
delta_millisecondsuint32Abstand zum vorherigen Ereignisstart
operation_countuint16Anzahl unmittelbar folgender Operationen
operationsvariabelexakt operation_count Operationen

Die erste delta_milliseconds MUSS 0 sein. Jede weitere MUSS mindestens minimum_frame_interval_milliseconds betragen. Die kumulierte Ereigniszeit muss streng steigen und kleiner als duration_milliseconds bleiben.

Operationen:

OpcodeNameNachfolgende Bytes
0x01SET_PIXELindex:uint16, r:uint8, g:uint8, b:uint8
0x02SET_RUNstart:uint16, count:uint16, r:uint8, g:uint8, b:uint8
0x03SET_RANGE_RGBstart:uint16, count:uint16, danach count × 3 RGB-Bytes
0x04FILLr:uint8, g:uint8, b:uint8

count MUSS größer als 0 sein; jeder adressierte Index muss innerhalb des Ausgangs liegen. Operationen eines Ereignisses werden in Wire-Reihenfolge auf den logischen Puffer angewendet und danach mit höchstens einem physischen Schreibvorgang ausgegeben.

Die erste Operation des ersten Ereignisses MUSS entweder FILL oder SET_RANGE_RGB mit start=0 und count=pixel_count sein. Damit definiert jede Timeline bei Zeit 0 einen absoluten Baselineframe. Unbekannte Opcodes, überzählige Bytes, vorzeitiges Datenende, ungültige Bereiche oder abweichende Ereigniszahl ergeben TIMELINE_INVALID_PROGRAM.

Verbindlicher Testvektor für vier Pixel:

Programmgröße: 28 Bytes
Base64: AAAAAAEABAD/ABQAAAACAAEAAACWAAEBAADIAA==
SHA-256: 37657360dd397ea89a19031042604b6c6d7816e2f55826dc6ca050e3bd59a6ea

Er dekodiert zu einem FILL(0,255,0) bei 0 ms und zwei SET_PIXEL-Operationen bei 20 ms.

14.6.4 Commit und Abbruch

Nach Übertragung aller Bytes sendet der Adapter:

{
  "type": "output.timeline.commit",
  "message_id": "adapter-54",
  "sequence": 54,
  "payload": {
    "timeline_id": "indicator-loop-7"
  }
}

Das Gerät MUSS Größe, SHA-256 und das vollständige Binärprogramm prüfen. Erst danach ersetzt es die bisher committed, nicht laufende Timeline dieses Ausgangs atomar. Ein Fehler darf die vorherige Timeline nicht verändern.

Eine abweichende empfangene Bytezahl ergibt TIMELINE_SIZE_MISMATCH, ein abweichender Hash TIMELINE_CHECKSUM_MISMATCH und ein syntaktisch oder semantisch ungültiges Programm TIMELINE_INVALID_PROGRAM. Fehlt die referenzierte Timeline beim Start, ergibt dies TIMELINE_NOT_FOUND.

{
  "type": "output.timeline.committed",
  "message_id": "device-55",
  "sequence": 55,
  "payload": {
    "reply_to": "adapter-54",
    "output_id": "main",
    "timeline_id": "indicator-loop-7",
    "program_sha256": "37657360dd397ea89a19031042604b6c6d7816e2f55826dc6ca050e3bd59a6ea"
  }
}

Ein Commit derselben bereits committed Timeline-ID mit identischem SHA-256 ist idempotent. Ein Staging-Upload kann mit output.timeline.abort und dem einzigen Payloadfeld timeline_id verworfen werden; das Gerät antwortet mit output.timeline.aborted, reply_to und timeline_id.

14.7 Timeline-Wiedergabe und Ausgangsstatus

14.7.1 Start

{
  "type": "output.timeline.play",
  "message_id": "adapter-56",
  "sequence": 56,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "timeline_id": "indicator-loop-7",
    "loop": true,
    "start_delay_milliseconds": 0
  }
}

start_delay_milliseconds ist ein uint16 von 0 bis 60000. Ein erfolgreicher Start ersetzt den Baselinezustand der Sitzung und liefert:

{
  "type": "output.timeline.playing",
  "message_id": "device-57",
  "sequence": 57,
  "payload": {
    "reply_to": "adapter-56",
    "output_id": "main",
    "timeline_id": "indicator-loop-7",
    "loop": true,
    "scheduled_start_uptime_milliseconds": 183000
  }
}

Die monotone Gerätezeit ist ein wrap-fähiger uint32. Startverzögerungen sind relativ zum Empfang und dienen nicht zur geräteübergreifenden Synchronisation.

Bei jeder Wiedergabe wird das erste Ereignis am Startzeitpunkt angewendet. Bei loop=true beginnt am exakten Ende von duration_milliseconds wieder Ereignis 0; dadurch kann kein Delta aus dem vorherigen Durchlauf fortwirken. Bei loop=false bleibt nach dem letzten Ereignis der letzte Frame bis zum Stopp oder nächsten mutierenden Befehl sichtbar.

Ist der Scheduler verspätet, MUSS er alle inzwischen fälligen Ereignisse in Reihenfolge auf den logischen Puffer anwenden, DARF aber nur den daraus resultierenden neuesten Frame physisch schreiben. Die Timelinezeit läuft von der geplanten Startzeit weiter und darf nicht durch Netzwerk- oder Renderverzögerungen dauerhaft verschoben werden.

14.7.2 Stopp

{
  "type": "output.timeline.stop",
  "message_id": "adapter-58",
  "sequence": 58,
  "payload": {
    "output_id": "main",
    "timeline_id": "indicator-loop-7",
    "behavior": "hold"
  }
}

behavior ist hold oder clear. Der Stopp ist idempotent. output.timeline.stopped spiegelt reply_to, output_id, timeline_id und behavior. Eine nicht geloopte Timeline sendet nach Erreichen ihrer Dauer einmal output.timeline.completed mit output_id und timeline_id; der letzte Frame bleibt sichtbar.

14.7.3 Status

Der Adapter fragt ab:

{
  "type": "output.status.get",
  "message_id": "adapter-60",
  "sequence": 60,
  "payload": {
    "output_id": "main"
  }
}

Das Gerät antwortet:

{
  "type": "output.status",
  "message_id": "device-61",
  "sequence": 61,
  "payload": {
    "reply_to": "adapter-60",
    "output_id": "main",
    "config_revision": 4,
    "mode": "timeline_playing",
    "frame_id": null,
    "timeline_id": "indicator-loop-7",
    "loop": true,
    "position_milliseconds": 3250
  }
}

mode ist idle, frame, timeline_scheduled oder timeline_playing. frame_id ist nur bei frame ein String. timeline_id, loop und position_milliseconds sind nur in den beiden Timelinemodi nicht null. Der Status verändert die Ausgabe nicht.

14.7.4 Laufzeithelligkeit

Wenn features.runtime_brightness=true ist, sendet der Adapter die dynamische Plugin-Helligkeit getrennt vom logischen RGB-Puffer:

{
  "type": "output.brightness.set",
  "message_id": "adapter-62",
  "sequence": 62,
  "payload": {
    "output_id": "main",
    "config_revision": 4,
    "brightness_percent": 72
  }
}

brightness_percent ist ein uint8 von 0 bis 100. Das Gerät multipliziert diesen Laufzeitfaktor mit dem konfigurierten Helligkeits- und Stromlimit erst beim physischen Rendern. Der logische RGB-Puffer sowie Frame-, Timeline- und Schedulerzustand dürfen dadurch nicht verändert werden.

Das Gerät bestätigt mit output.brightness.applied; der Payload enthält reply_to, output_id, config_revision, brightness_percent und applied_at_uptime_milliseconds. device.status.runtime_brightness spiegelt den aktiven Laufzeitfaktor. effective_brightness enthält bei dieser Capability bereits den resultierenden Gesamtfaktor einschließlich Laufzeithelligkeit.

14.8 config.changed

Nach erfolgreichem PUT /config sendet das Gerät:

{
  "type": "config.changed",
  "message_id": "device-44",
  "sequence": 44,
  "payload": {
    "config_revision": 5
  }
}

Bei aktiver Sitzung MUSS genau ein Ereignis gesendet werden. Ohne aktive Sitzung geht der Hinweis verloren; die Revision in device.hello übernimmt beim nächsten Verbindungsaufbau die Synchronisation. Der Adapter MUSS nach dem Ereignis GET /config ausführen.

RFC-6455-Ping/Pong-Control-Frames DÜRFEN jederzeit zusätzlich verwendet werden, auch zwischen Upgrade und homeess.hello. Ein Ping MUSS nach RFC 6455 mit einem Pong beantwortet werden; ein Pong MUSS ohne HDP-Fehler akzeptiert werden. Control-Frames besitzen kein HDP-Envelope, verbrauchen keine HDP-Sequenz und setzen weder den 5000-ms-Hello-Timeout noch den in Abschnitt 14.4 definierten Anwendungs-Heartbeat zurück.

14.9 Binary-I/O

Alle Nachrichten dieses Abschnitts sind ausschließlich bei aktivem binary-io-v1 erlaubt. GPIO-Nummern beziehen sich direkt auf die eindeutigen pins[].pin-Werte der aktiven Konfigurationsrevision.

14.9.1 Eingangssnapshot

Frühestens 1000 ms und spätestens 2000 ms nach session.ready sendet das Gerät genau einen entprellten Eingangssnapshot:

{
  "type": "binary.input.snapshot",
  "message_id": "device-3",
  "sequence": 3,
  "payload": {
    "config_revision": 7,
    "captured_at_uptime_milliseconds": 1842,
    "inputs": [
      { "pin": 4, "input_type": "switch", "state": false },
      { "pin": 5, "input_type": "button", "state": true }
    ],
    "outputs": [
      { "pin": 12, "state": false }
    ]
  }
}

Vor diesem Snapshot sendet das Gerät in der neuen Sitzung keine binary.input.event-Nachricht. Der Snapshot ist der Abgleichpunkt; offline oder vor dem Snapshot aufgetretene Tasterereignisse werden nicht gepuffert.

14.9.2 Aktive Eingangsereignisse

Nach jeder entprellten Schalteränderung sendet das Gerät:

{
  "type": "binary.input.event",
  "message_id": "device-8",
  "sequence": 8,
  "payload": {
    "pin": 4,
    "input_type": "switch",
    "event": "changed",
    "state": true,
    "event_sequence": 19,
    "occurred_at_uptime_milliseconds": 15220,
    "config_revision": 7
  }
}

Ein Taster sendet nur für den entprellten aktiven Übergang ein Ereignis mit input_type="button", event="pressed" und state=true. Das Loslassen wird entprellt und intern übernommen, erzeugt aber kein Ereignis.

event_sequence ist ein bootlokaler, bei 1 beginnender uint32-Zähler der erkannten Ereignisse. Beobachtete Lücken sind zulässig; der Zähler ist keine Exactly-once-Zustellgarantie und wird nach einem Boot zurückgesetzt. Der Adapter MUSS config_revision, Pinrichtung und Eingangstyp prüfen, bevor er das Ereignis verarbeitet.

Topics, Toggle-/Set-/Counterregeln, Zielwerte, Skalierungen und sonstige Automationssemantik sind ausdrücklich kein Bestandteil des Wire-Payloads. Sie MÜSSEN ausschließlich im Adapter gespeichert und ausgeführt werden.

14.9.3 Ausgang setzen

Der Adapter setzt einen Ausgang mit:

{
  "type": "binary.output.set",
  "message_id": "adapter-12",
  "sequence": 12,
  "payload": {
    "pin": 12,
    "state": true,
    "config_revision": 7
  }
}

Nach physischer Übernahme antwortet das Gerät:

{
  "type": "binary.output.applied",
  "message_id": "device-13",
  "sequence": 13,
  "payload": {
    "reply_to": "adapter-12",
    "pin": 12,
    "state": true,
    "config_revision": 7,
    "applied_at_uptime_milliseconds": 16640
  }
}

Die Bestätigung MUSS den tatsächlich ausgegebenen logischen Zustand enthalten. Der Adapter DARF den Befehl nur an einen als output konfigurierten Pin senden.

14.9.4 Statusabfrage

binary.status.get verwendet ein leeres Payloadobjekt. Das Gerät antwortet mit binary.status; dessen Payload entspricht binary.input.snapshot, enthält zusätzlich reply_to und listet sowohl Eingänge als auch Ausgänge. Die Abfrage verändert keinen Pin.

14.9.5 Fehler und Offline-Verhalten

  • unbekannter Pin: BINARY_PIN_NOT_FOUND;
  • als Eingang konfigurierter Zielpin: BINARY_PIN_DIRECTION_MISMATCH;
  • falsche Revision: BINARY_CONFIG_REVISION_MISMATCH;
  • mutierender Ausgangsbefehl während OTA: DEVICE_BUSY.

Bei Verlust der aktiven Steuersitzung setzt das Gerät alle Binary-Ausgänge ohne weitere Wire-Nachricht auf false. Nach der nächsten Sitzung MUSS der Adapter aus seinen Topicwerten erneut absolute Ausgangszustände senden.

14.10 OTA-Ereignisse

Erlaubte Typen:

firmware.update.started
firmware.update.progress
firmware.update.verifying
firmware.update.ready
firmware.update.failed
firmware.update.completed

OTA-Ereignisse sind Best-Effort-Hinweise auf einer gerade aktiven Sitzung. GET /firmware/status ist immer maßgeblich. Insbesondere kann firmware.update.completed wegen des Neustarts vor dem Aufbau der neuen WebSocket-Sitzung verloren gehen.

Jedes OTA-Ereignis verwendet dieses Payload:

{
  "type": "firmware.update.progress",
  "message_id": "device-51",
  "sequence": 51,
  "payload": {
    "target_version": "0.3.0",
    "received_bytes": 241152,
    "total_bytes": 482304,
    "progress_percent": 50,
    "error_code": null,
    "message": null
  }
}

target_version ist string|null; die Bytewerte sind uint32; progress_percent ist uint8 im Bereich 0…100. error_code und message sind nur bei firmware.update.failed Strings, sonst null.

14.11 WebSocket-Fehler

{
  "type": "error",
  "message_id": "device-52",
  "sequence": 52,
  "payload": {
    "reply_to": "adapter-50",
    "code": "TIMELINE_OFFSET_MISMATCH",
    "message": "Timeline chunk offset does not match.",
    "details": {
      "expected_offset": 512
    }
  }
}

reply_to ist die message_id des verursachenden Requests oder null, wenn kein einzelner Request zugeordnet werden kann.

FehlerklasseVerhalten nach Error-Nachricht
ungültiges Envelope, Binärframe oder Nachricht über ManifestlimitVerbindung schließen
SEQUENCE_ERRORVerbindung schließen
Hello-Fehler oder Hello-TimeoutVerbindung schließen
ungültiger Output-, Frame- oder TimelinebefehlVerbindung offen lassen
DEVICE_BUSYVerbindung offen lassen
UNSUPPORTED_MESSAGE_TYPEVerbindung offen lassen
HEARTBEAT_TIMEOUT oder SESSION_REPLACEDVerbindung schließen

14.18 Sensorwerte

sensor-reading-v1 sendet jede abgeschlossene Messung als sensor.sample. Physikalische Werte werden als Festkomma-Integer übertragen; benutzerdefinierte Skalierung und Kalibrierung gehören in den Adapter.

{
  "type": "sensor.sample",
  "message_id": "device-42",
  "sequence": 42,
  "payload": {
    "config_revision": 7,
    "sensor_id": "climate",
    "sensor_type": "bme280",
    "sample_sequence": 18,
    "captured_at_uptime_milliseconds": 928401,
    "status": "ok",
    "error": null,
    "values": {
      "temperature_millicelsius": 22430,
      "humidity_millipercent": 48720,
      "pressure_pascal": 100842
    }
  }
}

Erlaubte Messwertschlüssel sind temperature_millicelsius, humidity_millipercent, pressure_pascal, illuminance_millilux, bus_voltage_microvolts, shunt_voltage_microvolts, current_microamps, power_microwatts, distance_millimeters und raw.

sensor.status.get besitzt ein leeres Payloadobjekt. Das Gerät antwortet mit sensor.status; dessen samples-Array enthält den zuletzt abgeschlossenen Stand jedes konfigurierten Sensors. Anschließend fordert es für alle Sensoren eine neue Messung an. Eine fehlgeschlagene Messung wird mit status=error, einem stabilen error-Bezeichner und leerem values-Objekt gemeldet.

14.19 Fingerabdruckereignisse und Vorlagenverwaltung

fingerprint-event-v1 scannt nicht blockierend. Ein aufgelegter Finger darf bis zum Abheben höchstens ein fingerprint.match oder fingerprint.unknown auslösen. Eine positive Erkennung enthält config_revision, fortlaufende event_sequence, occurred_at_uptime_milliseconds, template_id und confidence. Der Adapter entscheidet allein, welcher State dadurch gesetzt, umgeschaltet oder um einen Schritt verändert wird.

Der Adapter startet das Lernen mit fingerprint.enroll.begin und den Feldern config_revision und template_id. Das Gerät meldet den Zweifach-Aufnahmeablauf als fingerprint.enroll.status; die stabilen Stufen sind place_finger, remove_finger, place_finger_again, complete, cancelled und enrollment. fingerprint.enroll.cancel bricht ab. fingerprint.template.delete löscht genau eine template_id. Jeder angenommene Befehl wird mit fingerprint.command.accepted und reply_to bestätigt.

fingerprint.status.get hat ein leeres Payloadobjekt. fingerprint.status enthält online, enrolling, capacity, template_count, das eindeutige Array occupied_slots und last_error. Eine Inventur nach Sitzungsaufbau sowie nach Lernen oder Löschen verhindert, dass der Adapter Vorlagenbelegungen erfindet. Die Vorlagen-ID ist eine lokale Referenz und kein biometrisches Datum.

15. Firmware-Update per OTA

15.1 Metadaten

GET /api/v1/firmware:

{
  "ok": true,
  "data": {
    "name": "hdp-firmware",
    "version": "0.2.0",
    "channel": "development",
    "platform": "esp8266",
    "board": "d1_mini",
    "variant": "generic",
    "build_id": "20260728-181524",
    "build_timestamp": "2026-07-28T18:15:24Z",
    "protocol_version": "1.0-draft",
    "config_schema_version": 1,
    "ota_supported": true,
    "ota_port": 8080,
    "maximum_image_size_bytes": 1044464,
    "free_update_space_bytes": 900000,
    "signature_verification": "not_configured",
    "signature_algorithm": null,
    "signature_key_id": null
  }
}

signature_verification ist enabled, not_configured oder unsupported. build_timestamp ist ein UTC-Zeitstempel im RFC-3339-Format. Versionsfelder verwenden SemVer 2.0.0.

Bei signature_verification == "enabled" ist signature_algorithm exakt ed25519-sha256 und signature_key_id ein nicht leerer ASCII-Identifier für den im Gerät hinterlegten Vertrauensschlüssel. Andernfalls sind beide Felder null.

15.2 OTA-Status

GET /api/v1/firmware/status:

{
  "ok": true,
  "data": {
    "state": "idle",
    "target_version": null,
    "received_bytes": 0,
    "total_bytes": 0,
    "progress_percent": 0,
    "restart_required": false,
    "signature_status": null,
    "last_error": null
  }
}

Zustände:

idle
preparing
receiving
verifying
ready_to_restart
restarting
completed
failed

Zulässige Zustandsfolge:

idle|failed|completed
→ preparing
→ receiving
→ verifying
→ ready_to_restart
→ restarting
→ completed

Aus preparing, receiving, verifying, ready_to_restart oder restarting kann ein Fehler nach failed führen. Ein neuer Upload ist nur aus idle, failed oder completed zulässig.

Solange der Betriebszustand updating ist, ergeben PUT /config, POST /pairing/start, POST /pairing/confirm, POST /unpair, POST /restart, POST /factory-reset sowie alle mutierenden WebSocket-Nachrichten mit Präfix output. sowie binary.output.set den Fehler DEVICE_BUSY. output.status.get und HTTP-Leseoperationen bleiben zulässig. POST /firmware/restart bleibt in ready_to_restart ausdrücklich zulässig.

target_version ist ein SemVer-String oder null, wenn noch keine syntaktisch gültige Zielversion bekannt ist. received_bytes und total_bytes sind uint32; progress_percent ist uint8 von 0 bis 100. restart_required ist genau in ready_to_restart und restarting wahr. signature_status ist einer der Werte signature_verified, signature_not_configured, signature_invalid oder null. last_error ist ausschließlich im Zustand failed ein Objekt mit den Pflichtfeldern code und message, sonst null.

15.3 Firmwareübertragung

Endpoint:

POST http://<device-ip>:<ota_port>/api/v1/firmware/update

Pflichtheader:

Content-Type: application/octet-stream
Content-Length: <exact image bytes>
X-HDP-Instance: <instance_id>
X-HDP-Binding-Key: <binding_key>
X-HDP-Firmware-Name: <name>
X-HDP-Firmware-Version: <semver>
X-HDP-Firmware-Channel: stable|beta|development
X-HDP-Platform: <platform>
X-HDP-Board: <board>
X-HDP-Variant: <variant>
X-HDP-Protocol-Version: 1.0-draft
X-HDP-Config-Schema-Version: <uint16>
X-HDP-Firmware-Size: <exact image bytes>
X-HDP-Firmware-SHA256: <64 lowercase hex>

Content-Type MUSS exakt application/octet-stream sein; andernfalls gilt UNSUPPORTED_MEDIA_TYPE.

Optionale Header:

X-HDP-Firmware-Signature: <detached signature>
X-HDP-Allow-Downgrade: true|false
X-HDP-Restart-After-Success: true|false

Fehlende Boolean-Header bedeuten false.

Ist signature_verification == "enabled", ist X-HDP-Firmware-Signature Pflicht. Der Wert ist die kanonische, gepaddete Base64-Kodierung nach RFC 4648 einer 64-Byte-Ed25519-Signatur. Signiert werden exakt die 32 rohen Bytes, die durch Dekodieren von X-HDP-Firmware-SHA256 entstehen. Fehlende, syntaktisch falsche oder nicht zum in /firmware genannten Schlüssel passende Signaturen ergeben OTA_SIGNATURE_INVALID.

Bei not_configured oder unsupported DARF der Header fehlen und wird vom Gerät nicht als Vertrauensnachweis verwendet. Der Adapter MUSS in diesem Fall die Authentizität des Releaseartefakts vor dem Upload über seinen Releasekanal prüfen.

Vor dem ersten Flash-Write MUSS das Gerät prüfen:

  • Owner-Authentifizierung,
  • Firmwarefamilie, Plattform, Board und Variante,
  • exakte Protokollversion,
  • kompatibles Config-Schema,
  • gültiges SemVer,
  • Downgrade-Regel,
  • Imagegröße gegen statisches und aktuelles Limit,
  • syntaktisch gültigen SHA-256.

Während des Streams:

  • Header-Timeout: 5000 ms;
  • maximale Pause zwischen zwei Datenblöcken: 10000 ms;
  • empfangene Bytes MÜSSEN gleichzeitig gehasht und geschrieben werden;
  • deklarierte, HTTP- und tatsächlich empfangene Länge MÜSSEN identisch sein.

Nach vollständigem Empfang:

  1. SHA-256 prüfen;
  2. Signatur prüfen, wenn signature_verification == "enabled";
  3. persistenten Pending-Validation-Datensatz schreiben;
  4. Firmwareimage finalisieren;
  5. Zustand ready_to_restart setzen.

Response HTTP 202:

{
  "ok": true,
  "data": {
    "state": "ready_to_restart",
    "restart_required": true
  }
}

Bei X-HDP-Restart-After-Success: false bleibt das Gerät in ready_to_restart, bis der Adapter den Restart-Endpoint aufruft. Bei true wechselt es unmittelbar nach der Response zu restarting und startet nach 500…2000 ms neu; der Adapter DARF dann keinen zusätzlichen Restart senden.

15.4 OTA-Neustart

POST /api/v1/firmware/restart ist ausschließlich in ready_to_restart zulässig.

Request:

{}

Response HTTP 202:

{
  "ok": true,
  "data": {
    "state": "restarting"
  }
}

Nach Rediscovery MUSS der Adapter /firmware und /firmware/status abfragen. Erfolg liegt nur vor, wenn die laufende Version der Zielversion entspricht und der Status completed meldet.

15.5 OTA-Wiederholung

FehlerklasseAutomatische Wiederholung
Verbindung vor erstem Byte fehlgeschlagenerlaubt
Transfer unterbrochen/Timeouterlaubt, kompletter Neuversuch
Authentifizierungnicht erlaubt
Plattform/Board/Variantenicht erlaubt
Protokoll/Config-Schemanicht erlaubt
Größe/SHA/Signaturnicht erlaubt
ready_to_restart erreichtUpload nicht wiederholen

16. Fehlerregister

CodeHTTPBedeutung
INVALID_REQUEST400/WSungültiges JSON, Typ, Envelope oder Pflichtfeld
ENDPOINT_NOT_FOUND404API-Pfad unbekannt
METHOD_NOT_ALLOWED405Methode für bekannten API-Pfad unzulässig
UNSUPPORTED_MEDIA_TYPE415Content-Type fehlt oder ist unzulässig
PAYLOAD_TOO_LARGE413Request-Body überschreitet das Limit
UNSUPPORTED_PROTOCOL_VERSION426/WSProtokollversion inkompatibel
UNSUPPORTED_RUNTIME_PROFILEWSLaufzeitprofil inkompatibel
AUTH_REQUIRED401/WSOwner-Credentials fehlen oder sind falsch
INCOMPLETE_BINDING_CREDENTIALS400nur einer der beiden Binding-Header vorhanden
ALREADY_PAIRED409Gerät besitzt bereits ein anderes Binding
PAIRING_IN_PROGRESS409andere Pairing-Session aktiv
PAIRING_SESSION_EXPIRED410Session fehlt, ist abgelaufen oder verbraucht
INVALID_BINDING_KEY422Binding-Key nicht exakt 64 lowercase Hexzeichen
NOT_PAIRED409Aktion erfordert Kopplung
INVALID_CONFIGURATION422Konfiguration ungültig
CONFIG_REVISION_CONFLICT409erwartete Revision stimmt nicht
CONFIG_RECOVERY_REQUIRED409persistente Konfiguration muss vor Schreibzugriff manuell behandelt werden
OUTPUT_NOT_FOUNDWSoutput_id existiert in der aktiven Konfiguration nicht
OUTPUT_CONFIG_REVISION_MISMATCHWSBefehl bezieht sich nicht auf die aktive Konfigurationsrevision
INVALID_OUTPUT_STATEWSOutput-/Frame-Payload ungültig oder ID mit anderem Inhalt wiederverwendet
OUTPUT_BASE_FRAME_REQUIREDWSPatch ist ohne Baselineframe der Sitzung unzulässig
OUTPUT_RATE_LIMITEDWSphysische Framefrequenz würde das Manifestlimit verletzen
OUTPUT_BUSYWSOutput führt eine mit dem Befehl unvereinbare Timeline aus
BINARY_PIN_NOT_FOUNDWSGPIO gehört nicht zur aktiven Binary-Konfiguration
BINARY_PIN_DIRECTION_MISMATCHWSmutierender Befehl adressiert keinen Ausgang
BINARY_CONFIG_REVISION_MISMATCHWSBefehl bezieht sich nicht auf die aktive Binary-Konfigurationsrevision
FINGERPRINT_CONFIG_REVISION_MISMATCHWSBefehl bezieht sich nicht auf die aktive Fingerabdruckkonfigurationsrevision
FINGERPRINT_OPERATION_REJECTEDWSLernen, Abbruch oder Löschen ist im aktuellen Modulzustand nicht zulässig; message benennt Modul- oder Vorlagenursache, details.template_id die betroffene Vorlage, soweit vorhanden
TIMELINE_UPLOAD_IN_PROGRESSWSein anderer Staging-Upload ist in derselben Sitzung aktiv
TIMELINE_NOT_FOUNDWSreferenzierte Timeline ist nicht committed
TIMELINE_OFFSET_MISMATCHWSChunkoffset weicht von details.expected_offset ab
TIMELINE_SIZE_MISMATCHWSempfangene Programmlänge stimmt nicht mit den Metadaten überein
TIMELINE_CHECKSUM_MISMATCHWSSHA-256 des Timelineprogramms stimmt nicht
TIMELINE_INVALID_PROGRAMWSbinäres Timelineprogramm verletzt hdtl-delta-v1
TIMELINE_CAPACITY_EXCEEDEDWSManifestlimit oder verfügbarer Stagingspeicher reicht nicht
UNSUPPORTED_MESSAGE_TYPEWSNachrichtentyp unbekannt
SEQUENCE_ERRORWSSequenz nicht exakt monoton
SESSION_HELLO_TIMEOUTWShomeess.hello nicht innerhalb von 5000 ms empfangen
HEARTBEAT_TIMEOUTWS45000 ms keine gültige Adapter-Nachricht
SESSION_REPLACEDWSneue authentifizierte Steuersitzung hat die alte ersetzt
DEVICE_BUSY423/WSZustandsänderung während laufender OTA-Transaktion
FACTORY_RESET_NOT_ALLOWED403Gerät nicht im Recovery-Modus
INTERNAL_ERROR500/WSpersistenter oder interner Fehler
OTA_AUTH_REQUIRED401OTA-Owner-Authentifizierung fehlgeschlagen
OTA_ALREADY_RUNNING423OTA bereits aktiv oder restartbereit
OTA_INVALID_METADATA400/422Metadaten fehlen oder sind ungültig
OTA_FIRMWARE_NAME_MISMATCH422falsche Firmwarefamilie
OTA_PLATFORM_MISMATCH422falsche Plattform
OTA_BOARD_MISMATCH422falsches Board
OTA_VARIANT_MISMATCH422falsche Variante
OTA_PROTOCOL_INCOMPATIBLE422Protokoll inkompatibel
OTA_CONFIG_SCHEMA_INCOMPATIBLE422Config-Schema nicht migrierbar
OTA_DOWNGRADE_NOT_ALLOWED422Downgrade nicht freigegeben
OTA_IMAGE_TOO_LARGE413statisches Imagelimit überschritten
OTA_INSUFFICIENT_SPACE413aktuell zu wenig OTA-Platz
OTA_TRANSFER_FAILED400Verbindung oder Inter-Chunk-Timeout
OTA_SIZE_MISMATCH400Längen stimmen nicht überein
OTA_CHECKSUM_MISMATCH422SHA-256 stimmt nicht
OTA_SIGNATURE_INVALID422Signatur ungültig
OTA_WRITE_FAILED500Flash-Schreibfehler
OTA_FINALIZE_FAILED500Finalisierung/Persistierung fehlgeschlagen
OTA_RESTART_NOT_READY409kein verifiziertes Image restartbereit
OTA_BOOT_VALIDATION_FAILED500Validierung nach Neustart fehlgeschlagen

Nicht registrierte Fehlercodes sind in HDP 1.0-draft unzulässig.

17. Verbindliche Timeouts und Retryregeln

17.1 Adapter-Timeouts

OperationConnect-TimeoutResponse-Timeout
mDNS-Auflösung3000 ms
öffentliche GETs2000 ms5000 ms
Pairing start/confirm/status2000 ms5000 ms
GET/PUT config2000 ms10000 ms
unpair/restart/factory-reset2000 ms5000 ms
WebSocket-Upgrade3000 ms5000 ms bis session.ready
output.frame.set5000 ms
binary.output.set, binary.status.get5000 ms
Timeline begin/chunk/play/stop/status5000 ms
Timeline commit10000 ms
OTA-Metadaten/Status2000 ms5000 ms
OTA-Upload5000 msnach Abschnitt 15.3
Rediscovery nach Neustart60000 ms

17.2 Allgemeine Wiederholung

  • GET-Requests DÜRFEN höchstens dreimal mit 250 ms, 500 ms und 1000 ms

Wartezeit plus 0…100 ms Jitter wiederholt werden.

  • Zustandsändernde Requests DÜRFEN nur nach den ausdrücklich beschriebenen

Recoveryregeln wiederholt werden.

  • POST /pairing/confirm ist mit identischem Payload idempotent.
  • PUT /config wird nach Abschnitt 12.5 abgeglichen.
  • POST /restart, /unpair und /factory-reset werden nach Timeout nicht blind

wiederholt.

  • HTTP 4xx wird nicht automatisch wiederholt, außer ausdrücklich beschrieben.
  • HTTP 5xx DARF maximal einmal nach 1000…2000 ms wiederholt werden, sofern die

Operation idempotent oder vorher abgeglichen ist.

17.3 Operationsspezifische Wiederherstellung

Unsicherer AusgangVerbindliche Adapteraktion
/pairing/start ohne Responseexakt denselben Request innerhalb 120000 ms höchstens zweimal erneut senden
/pairing/confirm ohne Responsezuerst /pairing/status mit Pending-Credentials; bei match aktivieren, bei unpaired identischen Confirm erneut senden, bei conflict abbrechen
PUT /config ohne Responseausschließlich Verfahren aus Abschnitt 12.5
output.frame.set ohne Responsemit derselben frame_id und byteidentischem Payload einmal erneut senden; bei erneutem Verbindungsverlust nach neuer Sitzung absoluten Replace-Frame senden
binary.output.set ohne Responsenach neuer Sitzung binary.status.get senden und nur bei abweichendem Ausgangszustand den absoluten Befehl erneut senden
Timeline begin/chunk/commit ohne Responsein derselben Sitzung denselben idempotenten Request wiederholen; nach Sitzungsverlust Upload mit begin und Offset 0 neu beginnen
output.timeline.play ohne Responseoutput.status.get senden; nur starten, wenn nicht bereits dieselbe timeline_id mit demselben loop-Wert läuft oder geplant ist
output.timeline.stop ohne Responseoutput.status.get senden; bei idle oder frame als Erfolg behandeln, andernfalls denselben Stopp wiederholen
/unpair ohne Responsebis 60000 ms rediscovern und /pairing/status prüfen; bei unpaired als Erfolg behandeln, sonst Benutzerentscheidung
/restart ohne Responsebis 60000 ms rediscovern; Request nicht wiederholen
/factory-reset ohne Responsealten Fund bis 60000 ms beobachten und nach neuer device_id suchen; Request nicht wiederholen
/firmware/restart ohne Responsebis 60000 ms rediscovern und Version/Status prüfen; Request nicht wiederholen

Nach unerwartetem WebSocket-Verlust mit weiterhin aktivem Binding verwendet der Adapter folgende Reconnect-Abstände:

1000 ms, 2000 ms, 5000 ms, 10000 ms, danach 30000 ms

Zu jedem Wert kommen 0…250 ms Jitter. Vor jedem Versuch MUSS die aktuelle Geräteadresse über den vorhandenen mDNS-Fund aktualisiert werden. Nach AUTH_REQUIRED oder einem Binding-conflict werden automatische Reconnects beendet. Nach SESSION_REPLACED wartet die ersetzte Sitzung mindestens 30000 ms und darf nur erneut verbinden, wenn die eigene Adapterinstanz weiterhin die aktive Steuerrolle beansprucht; dadurch wird ein gegenseitiges Verdrängen zweier lokaler Prozesse verhindert.

18. Konformitätsanforderungen

Eine Adapter- und Firmwarekombination gilt erst dann als konform, wenn mindestens folgende Ende-zu-Ende-Fälle bestanden sind:

  1. Discovery eines ungekoppelten Geräts.
  2. Erfolgreiches Pairing mit übereinstimmender binding_id.
  3. Verlorene Confirm-Response und Wiederaufnahme über /pairing/status.
  4. Fremde Owner-ID bei gleichem Key ergibt conflict.
  5. Gleiche Owner-ID bei falschem Key ergibt conflict.
  6. Nur ein Binding-Header ergibt INCOMPLETE_BINDING_CREDENTIALS.
  7. Authentifizierter und nicht authentifizierter Zugriff auf jede A-Route.
  8. Konfigurationsschreiben mit korrekter Revision.
  9. Konfigurationskonflikt und verlorene PUT-Response.
  10. Neustart mit erhaltener Kopplung und Hardwarekonfiguration.
  11. Entkopplung mit erhaltener Hardwarekonfiguration.
  12. WebSocket-Handshake, Sequenzprüfung, Heartbeat und Reconnect.
  13. Boot-Umschaltung zwischen pixel-timeline-v1 und binary-io-v1 ohne

Vermischung der Pin-Treiber.

  1. 30-ms-Entprellung, Schalteränderung, ausschließliches Taster-Pressed-Ereignis

und Eingangssnapshot nach Sitzungsaufbau.

  1. Binary-Ausgangsbestätigung sowie LOW-Sicherheitszustand bei Boot und

Controllerverlust.

  1. Runtime-Profilabgleich und Ablehnung eines inkompatiblen Profils.
  2. Absoluter Replace-Frame, idempotente Wiederholung, Patch und fehlender

Baselineframe.

  1. Ablehnung falscher Output-ID, Konfigurationsrevision, Framegröße und

Pixeldatentypen ohne Teilanwendung.

  1. Chunkweiser Upload des verbindlichen Timeline-Testvektors einschließlich

Offset-, Größen-, Hash- und Programmfehlern.

  1. Timeline-Start, Loopgrenze, Scheduler-Aufholen, Stopp, Statusabgleich und

Wiederaufnahme nach verlorener Response.

  1. Offlineverhalten und Verlust aller flüchtigen Frames, Timelines und

Stagingdaten nach Neustart bei unverändertem Binding und unveränderter Hardwarekonfiguration.

  1. Helligkeits- und Strombegrenzung ohne Veränderung des logischen Puffers.
  2. OTA-Erfolg, Transferabbruch, falsche Größe und falscher SHA-256.
  3. Rediscovery und Versionsbestätigung nach OTA.

Beispiele in diesem Dokument sind normativ hinsichtlich Feldnamen, Datentypen und Einheiten. Beispielwerte sind nicht normativ.

WordPress Appliance - Powered by TurnKey Linux