homeESS Audio Bus

Der Audio Bus ist die zentrale, bidirektionale Audio-Infrastruktur von homeESS. Externe Clients (Desktop-Widget, App, Browser, Sprachsatellit …) streamen Audio an homeESS und empfangen Audio zurück; interne Plugins abonnieren laufende Audio-Sessions und senden selbst Audio.

Der Bus ist Transport und Routing – kein Audio-Processing. Er transcodiert nicht, zeichnet nichts auf und transportiert Audio nie über das State-System. Spracherkennung, Sprachausgabe, Assistenten und Ähnliches bauen als Plugins auf ihm auf.

 Client ── Auth + WebSocket ──▶ homeESS Audio Bus ── interne Plugin-API ──▶ Plugins
        ◀──── Output-Audio ────  (Sessions, Routing,  ◀── Output-Audio ────
                                  Metadaten, Streams)

Inhalt

  1. Endpunkt und Anmeldung
  2. Protokoll
  3. Audioformate
  4. Grenzen und Timeouts
  5. Fehlercodes
  6. Beispiel-Client (Node.js)
  7. Plugin-API
  8. States, Status und Logging
  9. Bekannte Einschränkungen

1. Endpunkt und Anmeldung

ws://<homeess-host>:<port>/api/v1/audio/ws

Der Endpunkt liegt auf demselben HTTP-Server wie Weboberfläche und States API. Hinter einem TLS-Reverse-Proxy entsprechend wss://…/api/v1/audio/ws; der Proxy muss WebSocket-Upgrades durchreichen.

Anmeldung = States API. Es gibt keine eigenen Audio-Zugangsdaten:

  1. POST /api/v1/auth mit dem API-Passwort → Bearer-Token

(STATES-API.md, Abschnitt 3).

  1. WebSocket-Upgrade mit demselben Token im Header:
   GET /api/v1/audio/ws HTTP/1.1
   Upgrade: websocket
   Authorization: Bearer <token>

Token als Query-Parameter oder Cookie werden nicht akzeptiert. Ohne gültiges Token wird der Upgrade vor dem Aufbau der WebSocket-Verbindung mit dem JSON-Fehlerformat der States API abgewiesen:

HTTPerrorWann
401unauthorizedKein Authorization: Bearer …-Header
401token_invalidToken unbekannt, abgemeldet oder durch Passwortänderung ungültig
401token_expiredToken abgelaufen – neu anmelden
403api_disabledStates API ausgeschaltet
503too_many_connectionsZu viele gleichzeitige Audio-Verbindungen

Ein gewöhnlicher HTTP-Aufruf ohne Upgrade erhält 426 upgrade_required.

Das Token wird während der Verbindung laufend erneut geprüft (alle 5 s und vor jedem audio.start). Läuft es ab, meldet sich der Client ab, ändert der Administrator das API-Passwort oder schaltet er die API aus, sendet der Server audio.error (token_expired/token_invalid/api_disabled), beendet eine laufende Session (reason: "auth_lost") und schließt mit Close-Code 4401.

2. Protokoll

  • Control-Messages sind JSON-Textframes (höchstens 4 KiB).
  • Audio läuft ausschließlich als binärer WebSocket-Frame – in beide

Richtungen, nie Base64 in JSON.

  • Eine Verbindung führt höchstens eine aktive Session. Binärframes des

Clients gehören immer zu dieser Session. Mehrere parallele Sessions eines Geräts laufen über mehrere Verbindungen; nacheinander kann eine Verbindung beliebig viele Sessions führen.

  • Die Zuordnung Session ↔ Verbindung liegt nur im Server. Eine session_id

in Client-Nachrichten wählt nie eine fremde Session aus.

Ablauf

Client                                   Server
  │── Upgrade + Authorization ─────────────▶│
  │◀──────────────────────── audio.ready ───│
  │── audio.start ─────────────────────────▶│
  │◀────────────────────── audio.started ───│  session_id
  │══ Binärframe (Mikrofon) ═══════════════▶│  → Plugins
  │══ Binärframe … ════════════════════════▶│
  │◀───────────────── audio.output.start ───│  Format des Output
  │◀════════════════ Binärframe (Output) ═══│  ← Plugin
  │◀─────────────────── audio.output.end ───│
  │── audio.end ───────────────────────────▶│
  │◀──────────────────────── audio.ended ───│

Client → Server

audio.start – Session eröffnen.

{
  "type": "audio.start",
  "device_id": "widgetbar-buero-pc",
  "source": "widgetbar",
  "room": "buero",
  "codec": "pcm_s16le",
  "sample_rate": 16000,
  "channels": 1,
  "metadata": { "wake_word": "hey homeess" }
}
FeldPflichtRegel
device_idja1–128 Zeichen A–Z a–z 0–9 . _ : @ -, beginnt mit Buchstabe/Ziffer
sourceja1–64 Zeichen A–Z a–z 0–9 . _ - (Art des Clients, z. B. widgetbar, android, esp32)
roomneinbis 64 Zeichen, keine Steuerzeichen
codecjasiehe Audioformate
sample_ratejaganze Zahl, siehe Audioformate
channelsjaganze Zahl, siehe Audioformate
metadataneinObjekt, höchstens 16 Schlüssel ^[a-z][a-z0-9_]{0,31}$, Werte Text (≤ 256 Zeichen), Zahl oder Wahrheitswert

Alle Felder sind Clientangaben. Die authentifizierte Identität (Token) setzt der Server getrennt davon; sie lässt sich weder über Felder noch über metadata überschreiben. Eine mitgeschickte session_id wird ignoriert. Unbekannte Felder werden ignoriert.

audio.end – eigene Session beenden.

{ "type": "audio.end", "session_id": "550e8400-e29b-41d4-a716-446655440000" }

ping – Anwendungs-Keepalive, Antwort pong. (Zusätzlich sendet der Server WebSocket-Pings; ein Client ohne Pong wird getrennt.)

Binärframe – Audio-Input der aktiven Session.

Server → Client

typeFelderBedeutung
audio.readyprotocol, max_frame_bytes, idle_timeout_ms, max_session_ms, codecsNach dem Verbindungsaufbau: Protokollversion (1), Grenzen und unterstützte Formate
audio.startedsession_id, codec, sample_rate, channelsSession läuft; session_id ist eine serverseitig erzeugte UUID
audio.output.startsession_id, codec, sample_rate, channelsAb jetzt folgen Output-Binärframes in diesem Format (kann vom Input abweichen)
audio.output.endsession_idOutput-Stream beendet
audio.endedsession_id, reasonSession beendet (Bestätigung von audio.end oder serverseitiges Ende)
audio.errorcode, message, session_id?Fehler, siehe Fehlercodes
pong–Antwort auf ping

reason in audio.ended: client_end, idle_timeout, session_timeout, auth_lost, server_shutdown, protocol_error (bei disconnect ist der Client bereits weg).

Close-Codes: 1001 Server-Shutdown, 1008 Protokollverstöße/zu viele Control-Messages, 1009 Nachricht über 256 KiB, 4401 Anmeldung ungültig.

3. Audioformate

codecBeschreibungsample_ratechannels
pcm_s16lePCM signed 16 Bit little endian, interleaved8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 480001–2

Empfohlen für Sprache: pcm_s16le, 16000 Hz, mono. Ein PCM-Frame muss ganze Samples enthalten (Länge teilbar durch 2 × Kanäle). Input und Output tragen jeweils ein eigenes Format. Weitere Codecs (z. B. Opus) können ergänzt werden, ohne das Protokoll zu ändern (src/audio-bus/formats.js).

4. Grenzen und Timeouts

Unter Einstellungen → States API → Audio Bus können Administratoren die maximale Session-Anzahl dauerhaft speichern. Änderungen gelten ohne Neustart. Eine Absenkung trennt bestehende Sessions und Verbindungen nicht; neue werden erst zugelassen, wenn die jeweilige Anzahl unter dem neuen Limit liegt.

GrenzeWert
Gleichzeitige Sessions (gesamt)1–256 einstellbar, Standard 16
Gleichzeitige Audio-VerbindungenAutomatisch max(32, 2 × Session-Limit)
Input-Frame≤ 32 KiB (größere werden mit frame_too_large verworfen)
WebSocket-Nachricht (hart)≤ 256 KiB, sonst Close 1009
Control-Message≤ 4 KiB, ≤ 60 je 10 s
Protokollfehler je Verbindung> 20 → Close 1008
Idle-Timeout30 s ohne Input und ohne Output
Maximale Session-Dauer15 min
Output-Chunk eines Plugins≤ 64 KiB
Ungesendeter Output je Session≤ 1 MiB (darüber output_backpressure für das Plugin)
Queue je Plugin≤ 256 Chunks und ≤ 1 MiB

Empfohlene Frame-Länge: 20–100 ms Audio (16 kHz mono: 640–3200 Bytes).

5. Fehlercodes

audio.error lässt die Verbindung offen (Ausnahme: Anmeldung, zu viele Fehler). Codes:

codeBedeutung
invalid_json, invalid_message, message_too_large, unknown_typeControl-Message unlesbar, kein Objekt, zu groß oder unbekannter Typ
invalid_device_id, invalid_source, invalid_room, invalid_metadataFeld in audio.start ungültig
unsupported_codec, invalid_sample_rate, invalid_channelsFormat nicht unterstützt
session_activeaudio.start, obwohl auf der Verbindung schon eine Session läuft
no_active_sessionBinärframe ohne aktive Session
session_not_found, invalid_session_idaudio.end mit fehlender oder nicht zur Verbindung gehörender session_id
frame_too_large, invalid_frameInput-Frame zu groß, leer oder ohne ganze Samples
too_many_sessionsObergrenze gleichzeitiger Sessions erreicht
token_expired, token_invalid, api_disabledAnmeldung verloren (Verbindung wird geschlossen)

6. Beispiel-Client (Node.js)

const WebSocket = require('ws');

const BASE = 'http://homeess.local:3000/api/v1';
const { token } = await (await fetch(`${BASE}/auth`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ password: process.env.HOMEESS_API_PASSWORD }),
})).json();

const ws = new WebSocket('ws://homeess.local:3000/api/v1/audio/ws', {
  headers: { Authorization: `Bearer ${token}` },
});
let sessionId = null;

ws.on('open', () => ws.send(JSON.stringify({
  type: 'audio.start', device_id: 'demo-pc', source: 'demo', room: 'buero',
  codec: 'pcm_s16le', sample_rate: 16000, channels: 1,
})));

ws.on('message', (data, isBinary) => {
  if (isBinary) return speaker.write(data);          // Output-Audio
  const message = JSON.parse(data);
  if (message.type === 'audio.started') {
    sessionId = message.session_id;
    microphone.on('data', (pcm) => ws.send(pcm));     // Binärframes
  }
  if (message.type === 'audio.output.start') speaker.configure(message);
});

// Beenden:
ws.send(JSON.stringify({ type: 'audio.end', session_id: sessionId }));

7. Plugin-API

Plugins laufen im vertrauenswürdigen homeESS-Kontext und authentifizieren sich nicht. Es gibt zwei Zugänge mit derselben Schnittstelle:

  • Adapter (eigener Kindprozess): host.audio.*, freigeschaltet über

"audioBus": true im Manifest (siehe ADAPTER.md). Alle Aufrufe außer den on…-Abos liefern Promises.

  • Core-Module (im Hauptprozess):
  const audioBus = require('./src/audio-bus');
  const audio = audioBus.createClient('mein-plugin');

Hier arbeiten die Aufrufe synchron und werfen Fehler mit error.code.

MethodeZweck
onSessionStarted(handler)handler(session) bei jeder neuen Session. Liefert eine Abmeldefunktion.
onInput(handler)handler(session, chunk, info) für jeden Input-Frame aller Sessions. chunk ist ein Buffer (nur lesen), info = { seq, receivedAt, droppedBefore }.
onSessionEnded(handler)handler(session, reason) nach dem Ende einer Session.
startOutput(sessionId, { codec, sampleRate, channels })Output-Stream beginnen; der Client erhält audio.output.start.
sendAudio(sessionId, chunk)Audio-Chunk (Buffer/Uint8Array, ≤ 64 KiB, ganze Samples) an genau den Client der Session senden.
endOutput(sessionId)Output-Stream beenden; der Client erhält audio.output.end.
getSession(sessionId)Session oder null.
listSessions()Alle aktiven Sessions.
close()(Core) Alle Abos lösen, eigene Output-Streams beenden. Adapter: automatisch beim Stoppen der Instanz.

Session-Objekt (eingefroren; neue Felder können hinzukommen):

{
  sessionId: '550e8400-…',
  status: 'active',                     // oder 'ended'
  client: { type: 'states-api', id: '<Token-Kennung>', remoteAddress: '192.168.1.20' },
  deviceId: 'widgetbar-buero-pc',       // Clientangabe
  source: 'widgetbar',                  // Clientangabe
  room: 'buero',                        // Clientangabe oder null
  codec: 'pcm_s16le', sampleRate: 16000, channels: 1,
  metadata: { wake_word: 'hey homeess' },
  createdAt: 1790000000000, lastInputAt: 1790000000500,
  endedAt: null, endReason: null,
  output: null,                         // oder { owner, codec, sampleRate, channels, startedAt }
}

client ist die serverseitig geprüfte Identität: id ist eine nicht umkehrbare Kennung des ausstellenden States-API-Tokens (nicht das Token).

Verhalten und Regeln

  • Jedes Plugin hat eine eigene, geordnete Ereignis-Queue. Callbacks dürfen ein

Promise liefern; das nächste Ereignis folgt erst danach. Ein langsames Plugin blockiert weder den WebSocket-Empfang noch andere Plugins.

  • Läuft die Queue eines Plugins über, werden **neue Audio-Chunks für dieses

Plugin verworfen**. info.droppedBefore des nächsten zugestellten Chunks nennt die Anzahl, info.seq zeigt die Lücke. Start- und Ende-Ereignisse gehen nicht verloren; onSessionEnded folgt immer nach dem letzten Input dieser Session.

  • Ein Callback, der länger als 10 s nicht antwortet, gilt als Fehler; die

Queue läuft weiter.

  • Je Session sendet höchstens ein Plugin gleichzeitig Output. Ein zweites

erhält output_busy, bis das erste endOutput() aufruft.

  • Output-Fehler (error.code): session_not_found, output_not_started,

output_active, output_busy, unsupported_codec, invalid_sample_rate, invalid_channels, invalid_chunk, chunk_too_large, client_disconnected, output_backpressure (Client nimmt nicht schnell genug ab – kurz warten und erneut senden), bei Adaptern zusätzlich audio_not_permitted.

Minimalbeispiel (Adapter, Echo):

module.exports = (host) => ({
  start() {
    host.audio.onSessionStarted(async (session) => {
      await host.audio.startOutput(session.sessionId, { codec: 'pcm_s16le', sampleRate: 16000, channels: 1 });
    });
    host.audio.onInput(async (session, chunk) => {
      await host.audio.sendAudio(session.sessionId, chunk);
    });
  },
});

8. States, Status und Logging

  • State: system://homeess/audio.active_sessions (Kategorie Betrieb)

– Anzahl aktiver Sessions. Einzelne Sessions werden bewusst nicht als States angelegt; Details liefern getSession()/listSessions() und die Session-Ereignisse.

  • Status: Einstellungen → States API → Audio Bus zeigt Zustand,

WebSocket-URL, aktive Sessions, Verbindungen, Input-Abonnenten, Output-Streams und verworfene Chunks – ohne Geräte, Räume oder IDs. Programmatisch: require('./src/audio-bus').status().

  • Log ([audio-bus]): Session gestartet/beendet (Kurz-ID, Quelle, Format,

Grund), Client getrennt, Plugin-Fehler, Queue-Overflow, Protokollfehler – jeweils gedrosselt. Nie Audioinhalte, Tokens, Gerätenamen oder Räume; kein Eintrag pro Frame.

9. Bekannte Einschränkungen

  • Nur pcm_s16le; keine Transcodierung. Opus folgt bei Bedarf.
  • Browser können beim WebSocket-Aufbau keinen Authorization-Header setzen.

Ein Browser-Client braucht einen Vermittler (z. B. eine eigene Seite von homeESS) oder eine spätere Erweiterung der Anmeldung.

  • Nicht über den Fernzugriff-Tunnel (Relay) erreichbar; der Tunnel überträgt

keine WebSocket-Upgrades. Nutzung im lokalen Netz bzw. über eigenen Reverse-Proxy.

  • Eine aktive Session je Verbindung.
  • Sessions und Tokens liegen nur im Speicher; ein Neustart von homeESS beendet

alle Sessions (server_shutdown).

  • Keine eingebaute Ratenbegrenzung der Audio-Datenmenge je Client außer

Frame-, Queue- und Sessiongrenzen.

WordPress Appliance - Powered by TurnKey Linux