FHEMWEB/VoiceControl: Web-STT & Hardware-Wakeword

VoiceControl – Sprachsteuerung via Browser und Atom Echo s3r
Diese Lösung ermöglicht eine flexible Sprachsteuerung für FHEM. Sprache wird in Text (Speech-to-Text) umgewandelt und als Reading STT im Device global bereitgestellt. Dieses Reading kann anschließend zentral (z. B. über notify oder DOIF) ausgewertet werden.
Unterstützt werden nur Chrome-basierte Browser (Chrome, Edge, Fully Browser). Firefox und Chromium haben leider kein Backend.
Es gibt zwei unterschiedliche Wege zur Spracherfassung:
- Weg 1️⃣ (Software): Browser-basierte Komplettlösung
- Weg 2️⃣ (Hybrid): Hardware-Wakeword + Browser-Spracherkennung
Hilfe
- Forenthread zum VoiceControl Sprachsteuerung
Funktionen
Grundprinzip
- Sprache → Speech-to-Text
- Ergebnis → Reading
STTim Deviceglobal - Zentrale Logik verarbeitet Befehle
Betriebsarten
- Push-to-Talk (nur Browser)
- Always-On mit Wakeword
- Hardware-Wakeword (Hybrid)
Rückmeldungen
- Sprachausgabe (TTS)
- Visuelle Bubble im Browser
- Optional gezielte Rückmeldung per Client-ID
Weg 1️⃣: Browser-Lösung (voicecontrol.js)
Das Script nutzt nur die Google Web Speech API.
Bedienung
Push-to-Talk
- Button gedrückt halten
- Wakeword erforderlich (Standard: „James“)
- Nach Aktivierung ca.6sek Zeit für Befehl
Always-On
- Kurzer Klick aktiviert Dauerbetrieb
- Wakeword erforderlich (Standard: „James“)
- Nach Aktivierung ca.6sek Zeit für Befehl
- JS wird neu gestartet udn Schleife fängt von vorne an
Wakeword
- Standard:
james - Anpassbar im Script:
const wakewords = ["james"];
Installation
Datei kopieren
{ Svn_GetFile('contrib/voicecontrol.js', 'www/pgm2/voicecontrol.js') }
Einbindung
attr WEBphone JavaScripts www/pgm2/voicecontrol.js
Hinweis (HTTP ohne HTTPS)
Chrome benötigt Freigabe für Mikrofon:
chrome://flags/#unsafely-treat-insecure-origin-as-secure- Eigene URL hinzufügen
- Auf
Enabledsetzen
Weg 2️⃣: Hybrid-Lösung mit Fully Kiosk Browser + Atom Echo (voicecontrol_echo.js)
Diese Variante kombiniert Hardware und Software. Daher ist sie besonders geeignet für ein Tablet mit Fully Kiosk Browser oder ähnlich, welches das Webinterface dauerhaft anzeigt.
- Wakeword-Erkennung erfolgt auf dem ESP (Atom Echo s3r)
- Die eigentliche Sprachverarbeitung (Speech-to-Text) erfolgt im Browser
Funktionsweise
- Wakeword wird auf dem ESP erkannt
- FHEM erzeugt Event (z. B.
james_detected) - Browser empfängt Event via WebSocket
- Speech-to-Text startet im Browser
- Ergebnis wird an FHEM übertragen
Aktivierung
- Kurzer Klick auf Button aktiviert/deaktiviert System
- Baut WebSocket-Verbindung zu FHEM auf
- Kein Push-to-Talk-Modus
Installation
Datei kopieren
Die Datei voicecontrol_echo.js ist hier zu finden: VoiceControl Sprachsteuerung und muss nach www/pgm2/voicecontrol_echo.js kopiert werden.
Die echo_s3r.yaml ist für den Atom Echo s3r. Die Konfuguration ist weiter unten beschrieben.
Einbindung in Fhem
attr WEBtablet JavaScripts www/pgm2/voicecontrol_echo.js
- Das Skript voicecontrol_echo.js muss in dem FHEMWEB-Device angelegt werden, das für den Fully Kiosk Browser zuständig ist
attr WEBtablet additionalInform atom_echos3r_9888e00f4280,global
- atom_echos3r_9********** = da kommt das Wakeword an, bzw. der Devicename vom Echo
- global = das ist zum auswerten der Sprachausgabe wichtig
Konfiguration im Javascript
const DEVICE = "atom_echos3r_9888e00f4280";
const TRIGGER = "james_detected";
const FHEM_IP = "192.168.1.76:8085";
DEVICE→ FHEM-Device des ESPTRIGGER→ Event bei WakewordFHEM_IP→ FHEM-Server
Konfiguration in der Yaml
Damit sich der ESP mit eurem MQTT-Server und WLAN verbindet, müssen folgende Stellen angepasst werden.
wifi:
ssid: "YOUR_SSID"
password: "YOUR_PW"
fast_connect: true
mqtt:
broker: 192.168.1.76
port: 1884
username: "YOUR_USERNAME"
password: "YOUR_PW"
topic_prefix: atom_echo
Wakeword (ESP / ESPHome)
Das Wakeword wird direkt auf dem ESP definiert:
- Umsetzung über ESPHome
- Eine große Auswahl an Wakewords sind hier zu finden:
https://github.com/TaterTotterson/microWakeWords
Hier ein Beispiel, wie das Wakeword definiert wird.
micro_wake_word:
id: mww
microphone: echo_mic
models:
- model: "https://github.com/TaterTotterson/microWakeWords/raw/main/microWakeWordsV2/james.json"
id: james_model
probability_cutoff: 0.45
Kommunikation
- WebSocket-Verbindung zu FHEM
- Lauscht auf Device-Events
- Automatischer Reconnect bei Abbruch
Ablauf nach Wakeword
- System sagt „Ja?“
- Browser startet SpeechRecognition
- Nutzer spricht Befehl
- Befehl wird verarbeitet
- Rückmeldung „Erledigt“
Zentrale Auswertung (Logik)
Beide Wege schreiben in:
global:STT_output
Die Verarbeitung erfolgt zentral über ein notify. Ein Device wird in der Mappingtabelle eingetragen.
Beispiel:
"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer", cmdOn => "on", cmdOff => "off" }
Aufschlüsselung:
"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname", cmdOn => "on", cmdOff => "off" }
Beispiel: notify zur Steuerung
defmod n_VoiceControl notify global:STT_output:.* {\
my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
$cleanEvent //= $EVENT;;\
$clientId //= "unknown";;\
\
my @responses;;\
my %vacRooms = map { $_ => ucfirst($_) } qw(arbeitszimmer badezimmer esszimmer flur küche wohnzimmer);;\
my $onRegEx = qr/\b(an|ein|einschalten|starte|aktivier|aktiviere|öffne|öffnen|auf|hoch|lade)\b/;;\
my $offRegEx = qr/\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere|schließe|schließen|zu|runter)\b/;;\
\
# --- 1. Geräte-Liste ---\
my %devices = (\
"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer (an/aus)" },\
"esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium (an/aus)" },\
"küche:licht|lampe|deckenlampe" => { dev => "Deckenlampe_Kue", label => "Licht Küche (an/aus)" },\
"küche:radio" => { dev => "MPD", label => "Radio Küche(an/aus)", cmdOn => "play", cmdOff => "stop" },\
"wohnzimmer:licht|lampe|deckenlampe" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer (an/aus)" },\
"fernseher|tv" => { dev => "VuPlus", label => "Fernseher (an/aus)" },\
"rechner|pc" => { dev => "PC_Aktor", label => "PC (an/aus)" },\
"garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor (öffnen/schließen)", cmdOn => "open", cmdOff => "close" },\
"kaffee" => { dev => "Kaffeemaschine (an/aus)", label => "Kaffeemaschine" },\
"roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto (an/aus)", cmdOn => "charge" },\
\
# Komplexe Sonderfunktionen (erkennbar am "run"-Eintrag)\
"ambiente" => { dev => "LampeSzeneAlle", label => "Ambiente", run => sub {\
my ($d, $c, $on, $off) = @_;;\
if ($c =~ /(\d+)/) {\
fhem("set $d->{dev} brightness " . int($1 * 2.55));;\
return "$d->{label} auf $1 Prozent";;\
}\
fhem("set $d->{dev} " . ($off ? "off" : "on")) if $on || $off;;\
return $on || $off ? "$d->{label} " . ($off ? "aus" : "an") : undef;;\
}},\
"sauge|reinige|putze|staubsauger|roboter" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Reinige/Sauge (Robosauger)", run => sub {\
my ($d, $c) = @_;;\
my @found = grep { $c =~ /\b$_\b/ } keys %vacRooms;;\
fhem(@found ? "set $d->{dev} clean_segment " . join(",", map { $vacRooms{$_} } @found) : "set $d->{dev} start");;\
return @found ? "Reinigung gestartet in " . join(" und ", map { $vacRooms{$_} } @found) : "Staubsauger gestartet";;\
}},\
"ambilight" => { label => "Ambilight (umschalten)", run => sub {\
system("sshpass -p '1431Fhem1982' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\
return "Ambilight erledigt";;\
}}\
);;\
\
# --- 2. TEXTBEREINIGUNG ---\
my $text = lc($cleanEvent);;\
$text =~ s/^stt_output:\s*//;; \
$text =~ s/\[\d+\.?\d*\]//g;; \
$text =~ s/^\s+|\s+$//g;; \
\
# --- 3. SPEZIALFÄLLE (KI & HILFE) ---\
if ($text =~ /\bfrage\s+(.*)$/) {\
my $q = $1;;\
my ($sec,$min,$hour,$mday,$mon,$year) = localtime;;\
fhem(sprintf("set GeminiAI ask [Aktuelle Systemzeit: %02d.%02d.%04d %02d:%02d:%02d] %s", $mday, $mon+1, $year+1900, $hour, $min, $sec, $q));;\
fhem("sleep 2;; setreading global TTS_input erledigt");;\
return;;\
}\
\
if ($text =~ /(hilfe|kommandos|übersicht)/) {\
my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;"><b>Befehlsübersicht:</b><br><br>';;\
my %seen;;\
for my $k (sort keys %devices) {\
my $d = $devices{$k};;\
next if $seen{$d->{label}}++;;\
# Nur noch das Prozent-Suffix für das Ambiente-Licht wird dynamisch angehängt\
my $suffix = ($d->{run} && $k eq "ambiente") ? " [0-100%]" : "";;\
$h .= "• $d->{label}$suffix<br>";;\
}\
$h .= '<br><u>Staubsauger Räume</u><br>• ' . join(", ", sort values %vacRooms) . '<br></div>';;\
$h =~ s/'/\\"/g;;\
my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$clientId'){FW_okDialog('$h')}";;\
FW_directNotify("#FHEMWEB:$_", $js, "") for devspec2array("TYPE=FHEMWEB");;\
fhem("sleep 2;; setreading global TTS_input erledigt");;\
return;;\
}\
\
# --- 4. AKTIONSERKENNUNG LOOP ---\
my $global_on = $text =~ /$onRegEx/ ? 1 : 0;;\
my $global_off = $text =~ /$offRegEx/ ? 1 : 0;;\
\
my @parts = split(/\s*(?:dann|,)\s*|(?<=\ban\b)\s*und\s*|(?<=\baus\b)\s*und\s*/, $text);;\
@parts = ($text) if @parts == 1;;\
\
for my $part (@parts) {\
$part =~ s/^\s+|\s+$//g;;\
next unless $part;;\
\
my $is_on = $part =~ /$onRegEx/ ? 1 : ($part =~ /$offRegEx/ ? 0 : $global_on);;\
my $is_off = $part =~ /$offRegEx/ ? 1 : ($part =~ /$onRegEx/ ? 0 : $global_off);;\
\
# Trennung: Original-Part bleibt für "run"-Blöcke, clean_part kriegt die Füllwortbereinigung\
my $clean_part = $part;;\
$clean_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der|und|sowie|,)\b/ /g;;\
\
# Schleife durch die Geräte (Sonderfunktionen mit "run" werden zuerst bewertet)\
for my $key (sort { ($devices{$b}{run} ? 1:0) <=> ($devices{$a}{run} ? 1:0) } keys %devices) {\
my ($main, $must) = split(/:/, $key);;\
\
if ($part =~ /\b($main)\b/ || $clean_part =~ /\b($main)\b/) {\
next if $must && $part !~ /\b($must)\b/;;\
\
my $d = $devices{$key};;\
if ($d->{run}) {\
my $res = $d->{run}->($d, $part, $is_on, $is_off);;\
push @responses, $res if $res;;\
} elsif ($is_on || $is_off) {\
my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\
fhem("set $d->{dev} $fhem_cmd");;\
push @responses, "$d->{label} " . ($is_off ? "aus" : "an");;\
}\
}\
}\
}\
\
# --- 5. FINALE SPRACHAUSGABE ---\
fhem("sleep 2;; setreading global TTS_input erledigt") if @responses;;\
}
Beispiel: notify für Sprachausgabe mit dem TTS-Modul
defmod n_global_TTS_output notify global:TTS_output:.* { my $text = $EVENT;;;; $text =~ s/^TTS_output:\s*//;;;; fhem("set TTS tts $text") }
Unterschiede der Wege
| Feature | Browser | Hybrid (Atom Echo S3) |
|---|---|---|
| Wakeword | Browser | ESP (Hardware) |
| Push-to-Talk | Ja | Nein |
| Always-On | Ja (bedingt) | Ja |
| Architektur | Software | Software + Hardware |
| Stabilität | Sehr stabil | Sehr stabil (Wakeword extern) |
FireOS mit Fully Browser (Plus)
Damit die Spracherkennung funktioniert:
- Apps installieren:
Google Speech Recognition & Synthesis Activity Launcher (Für bessere Sprachausgabe)
- FireOS:
„Tastatur und Sprache“ → „Text-to-Speech“
- Fully Browser:
Enable JavaScriptInterface (PLUS) aktivieren
Bessere Stimme für Sprachausgabe (4 Googlestimmen):
Damit konnte ich die roboterhafte Stimme von Hans gegen eine bessere Stimme tauschen.
- App Activity Launcher aus Playstore installieren
- Die App starten
- Suche nach Google
- Öffne --> Spracherkennung und -Synthese von Google
- Öffne --> Sprache hinzufügen
- Starte Aktivität
- Deutsch downloaden
- Deutsch anklicken. Nun stehen 4 Stimmen zur Auswahl. 2 weibliche und 2 männliche Stimmen.
- Zum Abschluss noch per adb shell auf das Device connecten und folgendes eingeben:
settings put secure tts_default_synth com.google.android.tts
reboot
Wer zurück zu Amazon möchte:
settings put secure tts_default_synth com.ivona.tts.oem
reboot