Zum Inhalt springen

FHEMWEB/VoiceControl: Web-STT & Hardware-Wakeword: Unterschied zwischen den Versionen

Aus FHEMWiki
Schwatter (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Schwatter (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Zeile 179: Zeile 179:
<syntaxhighlight lang="perl">
<syntaxhighlight lang="perl">
defmod n_VoiceControl notify global:STT:.* {\
defmod n_VoiceControl notify global:STT:.* {\
     # Parsing: Befehl und ID trennen\
     # 1. VORBEREITUNG\
     my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
     my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
     $cleanEvent //= $EVENT;;\
     $cleanEvent //= $EVENT;;\
     $clientId  //= "unknown";;\
     $clientId  //= "unknown";;\
    my $event_lc = lc($cleanEvent);;\
\
\
    # Befehlmapping\
     my %lightRooms = (\
     my %simpleCmds = (\
         "esszimmer"  => { dev => "Lampe01_Ez",       label => "Licht Esszimmer" },\
         "stt: esszimmer licht an"  => "set Lampe01_Ez on",\
         "küche"      => { dev => "Deckenlampe_Kue", label => "Licht Küche" },\
        "stt: esszimmer licht aus" => "set Lampe01_Ez off",\
         "wohnzimmer" => { dev => "Lampe06_Dek",        label => "Licht Wohnzimmer" }\
         "stt: küche licht an"      => "set Deckenlampe_Kue on",\
        "stt: küche licht aus"    => "set Deckenlampe_Kue off",\
        "stt: fernseher an"        => "set VuPlus on",\
         "stt: fernseher aus"       => "set VuPlus off",\
        "stt: lade roberto"        => "set MQTT2_valetudo_FlusteredUnequaledFish charge",\
         "stt: ambilight"    => '{ system("sshpass -p \'1431Fhem1982\' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 \"/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh\"") }',\
     );;\
     );;\
\
\
     if (exists $simpleCmds{$event_lc}) {\
     my %vacRooms = (\
         fhem($simpleCmds{$event_lc});;\
        "arbeitszimmer" => "Arbeitszimmer",\
    }\
        "badezimmer" => "Badezimmer",\
        "esszimmer" => "Esszimmer",\
        "flur" => "Flur",\
        "küche" => "Küche",\
        "wohnzimmer" => "Wohnzimmer"\
    );;\
\
    my $onRegEx  = '\b(an|ein|einschalten|starte|aktivier|aktiviere)\b';;\
    my $offRegEx = '\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere)\b';;\
\
    my @commands = split(/\s*(?:und|dann|,)\s*/, lc($cleanEvent));;\
\
    # 2. DER BEFEHLS-LOOP\
    foreach my $cmd_part (@commands) {\
        \
        $cmd_part =~ s/^\s+|\s+$//g;;\
        \
        my $is_on  = ($cmd_part =~ /$onRegEx/) ? 1 : 0;;\
        my $is_off = ($cmd_part =~ /$offRegEx/) ? 1 : 0;;\
\
        $cmd_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der)\b//g;;\
\
        # --- INTENT: STAUBSAUGER ---\
        if ($cmd_part =~ /(reinige|sauge|putze|staubsauger|roboter)/) {\
            my @found = grep { $cmd_part =~ /\b$_\b/ } keys %vacRooms;;\
            if (@found) {\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish clean_segment " . join(",", map { $vacRooms{$_} } @found));;\
            } else {\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish start");;\
            }\
            next;;\
        } elsif ($cmd_part =~ /(lade|aufladen|dock|station|home)/) {\
            fhem("set MQTT2_valetudo_FlusteredUnequaledFish charge");;\
            next;;\
        }\
\
        # --- INTENT: FERNSEHER ---\
        if ($cmd_part =~ /(fernseher|tv|vuplus)/) {\
            fhem("set VuPlus " . ($is_off ? "off" : "on"));;\
            next;;\
        }\
\
        # --- INTENT: AMBIENTE ---\
        if ($cmd_part =~ /ambiente/) {\
            if ($cmd_part =~ /(\d+)/) {\
                my $b = ($1 > 255 ? 255 : ($1 < 1 ? 1 : $1));;\
                fhem("set LampeSzeneAlle brightness $b");;\
            } else {\
                fhem("set LampeSzeneAlle " . ($is_off ? "off" : "on"));;\
            }\
            next;;\
        }\
\
        # --- INTENT: AMBILIGHT ---\
        if ($cmd_part =~ /ambilight/) {\
            system("sshpass -p 'GEHEIMESPASSWORT' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\
            next;;\
        }\
\
        # --- INTENT: LICHT ---\
        my ($lightRoom) = grep { $cmd_part =~ /\b$_\b/ } keys %lightRooms;;\
         if ($lightRoom || $cmd_part =~ /(licht|lampe)/) {\
            my $dev = $lightRooms{$lightRoom}{dev} // "LampeSzeneAlle";;\
            fhem("set $dev " . ($is_off ? "off" : "on")) if ($is_on || $is_off);;\
            next;;\
        }\
\
        # --- HILFE (DYNAMISCH) ---\
        if ($cmd_part =~ /(hilfe|kommandos|übersicht)/) {\
\
            my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;">';;\
            $h .= '<b>Befehlsübersicht:</b><br><br>';;\
\
            # Licht\
            $h .= '<u>Licht</u><br>';;\
            for my $k (sort keys %lightRooms) {\
                $h .= "• ".$lightRooms{$k}{label}." an/aus<br>";;\
            }\
\
            # Staubsauger\
            $h .= '<br><u>Staubsauger</u><br>';;\
            $h .= "• Sauge [Raum]<br>";;\
            $h .= "  Räume: ".join(", ", map { ucfirst($_) } sort keys %vacRooms)."<br>";;\
            $h .= "• Lade Roberto<br>";;\
\
            # Sonstiges\
            $h .= '<br><u>Sonstiges</u><br>';;\
            $h .= "• Fernseher an/aus<br>";;\
            $h .= "• Ambiente [an|aus|1-255]<br>";;\
            $h .= "• Ambilight<br>";;\
\
            $h .= '</div>';;\
\
            $h =~ s/'/\\"/g;;\
\
            my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$clientId'){FW_okDialog('$h')}";;\
\
\
    # Valetudo\
            FW_directNotify("#FHEMWEB:$_", $js, "")\
    elsif ($event_lc =~ /reinige/) {\
                for devspec2array("TYPE=FHEMWEB");;\
        my %rooms = (\
        "arbeitszimmer" =>"Arbeitszimmer",\
        "badezimmer"    =>"Badezimmer",\
        "esszimmer"    =>"Esszimmer",\
        "flur"          =>"Flur",\
        "küche"        =>"Küche",\
        "wohnzimmer"    =>"Wohnzimmer");;\
        my @found = grep { index($event_lc, $_) != -1 } keys %rooms;;\
        fhem("set MQTT2_valetudo_FlusteredUnequaledFish clean_segment ".join(",", map { $rooms{$_} } @found)) if @found;;\
    }\
    # Lampenscene\
    elsif ($event_lc =~ /ambiente/) {\
        if    ($event_lc =~ /(\d+)/) { my $b = ($1 > 255 ? 255 : ($1 < 1 ? 1 : $1));; fhem("set LampeSzeneAlle brightness $b") }\
        elsif ($event_lc =~ /an/)    { fhem("set LampeSzeneAlle on") }\
        elsif ($event_lc =~ /aus/)    { fhem("set LampeSzeneAlle off") }\
    }\
    # Kommandoübersicht\
    elsif ($event_lc =~ /kommandos/) {\
        my $h = '<div style="text-align:left;;min-width:200px;;font-family:sans-serif;;"><b>Befehlsübersicht:</b><br><br>';;\
        $h .= "• ".($_ =~ s/^stt: //r)."<br>" for sort keys %simpleCmds;;\
        $h .= "• reinige [küche, bad, ...]<br>• ambiente [an|aus|0-255]</div>";;\
        $h =~ s/'/\\"/g;;\
\
\
        my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$clientId'){FW_okDialog('$h')}";;\
            next;;\
         FW_directNotify("#FHEMWEB:$_", $js, "")\
         }\
            for devspec2array("TYPE=FHEMWEB");;\
     }\
     }\
}
}
Zeile 252: Zeile 317:
| Ja (bedingt)
| Ja (bedingt)
| Ja
| Ja
|-
| Audioaufnahme
| Möglich
| Nur nach Trigger
|-
|-
| Architektur
| Architektur
Zeile 262: Zeile 323:
|-
|-
| Stabilität
| Stabilität
| Stabil
| Sehr stabil
| Sehr stabil (Wakeword extern)
| Sehr stabil (Wakeword extern)
|}
|}

Version vom 3. April 2026, 09:50 Uhr

Mini
Mini

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.

Das System unterstützt zwei unterschiedliche Wege zur Spracherfassung:

  • Weg 1️⃣ (Software): Browser-basierte Komplettlösung
  • Weg 2️⃣ (Hybrid): Hardware-Wakeword + Browser-Spracherkennung

Hilfe

Dort befinden sich in Post #1 auch alle benötigten Dateien.


Funktionen

Grundprinzip

  • Sprache → Speech-to-Text
  • Ergebnis → Reading STT im Device global
  • 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 die Google Web Speech API. Unterstützt werden Chromium-basierte Browser (Chrome, Edge, Fully Browser). Firefox wird aktuell nicht unterstützt.

Bedienung

Push-to-Talk

  • Button gedrückt halten (~450 ms)
  • Direkt sprechen (kein Wakeword nötig)
  • Befehl wird sofort verarbeitet

Always-On

  • Kurzer Klick aktiviert Dauerbetrieb
  • Wakeword erforderlich (Standard: „James“)
  • Nach Aktivierung permanentes Mithören

Wakeword

  • Standard: james
  • Anpassbar im Script:
const wakewords = ["james"];

Ablauf:

  1. „James“ sagen
  2. System antwortet „Ja?“
  3. Zeitfenster (~6 Sekunden) für Befehl
  4. Danach automatische Verarbeitung oder Abbruch


Installation

Datei kopieren

voicecontrol.js nach: /opt/fhem/www/voicecontrol/

Einbindung

attr WEBphone JavaScripts voicecontrol/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 Enabled setzen


Weg 2️⃣: Hybrid-Lösung mit Browser + Atom Echo (voicecontrol_echo.js)

Diese Variante kombiniert Hardware und Software:

  • Wakeword-Erkennung erfolgt auf dem ESP (Atom Echo s3r)
  • Die eigentliche Sprachverarbeitung (Speech-to-Text) erfolgt im Browser

Funktionsweise

  1. Wakeword wird auf dem ESP erkannt
  2. FHEM erzeugt Event (z. B. james_detected)
  3. Browser empfängt Event via WebSocket
  4. Speech-to-Text startet im Browser
  5. Ergebnis wird an FHEM übertragen

Aktivierung

  • Kurzer Klick auf Button aktiviert/deaktiviert System
  • Baut WebSocket-Verbindung zu FHEM auf
  • Kein Push-to-Talk-Modus

Konfiguration im Javascript

const DEVICE  = "atom_echos3r_9888e00f4280";
const TRIGGER = "james_detected";
const FHEM_IP = "192.168.1.76:8085";
  • DEVICE → FHEM-Device des ESP
  • TRIGGER → Event bei Wakeword
  • FHEM_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.6   

Kommunikation

  • WebSocket-Verbindung zu FHEM
  • Lauscht auf Device-Events
  • Automatischer Reconnect bei Abbruch

Ablauf nach Wakeword

  1. System sagt „Ja?“
  2. Browser startet SpeechRecognition
  3. Nutzer spricht Befehl
  4. Befehl wird verarbeitet
  5. Rückmeldung „Erledigt“

Installation

siehe oben wie unter Punkt 1.

Zentrale Auswertung (Logik)

Beide Wege schreiben in:

global:STT

Die Verarbeitung erfolgt zentral über ein notify.

Beispiel: notify

defmod n_VoiceControl notify global:STT:.* {\
    # 1. VORBEREITUNG\
    my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
    $cleanEvent //= $EVENT;;\
    $clientId   //= "unknown";;\
\
    my %lightRooms = (\
        "esszimmer"  => { dev => "Lampe01_Ez",       label => "Licht Esszimmer" },\
        "küche"      => { dev => "Deckenlampe_Kue",  label => "Licht Küche" },\
        "wohnzimmer" => { dev => "Lampe06_Dek",         label => "Licht Wohnzimmer" }\
    );;\
\
    my %vacRooms = (\
        "arbeitszimmer" => "Arbeitszimmer",\
        "badezimmer" => "Badezimmer",\
        "esszimmer" => "Esszimmer",\
        "flur" => "Flur",\
        "küche" => "Küche",\
        "wohnzimmer" => "Wohnzimmer"\
    );;\
\
    my $onRegEx  = '\b(an|ein|einschalten|starte|aktivier|aktiviere)\b';;\
    my $offRegEx = '\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere)\b';;\
\
    my @commands = split(/\s*(?:und|dann|,)\s*/, lc($cleanEvent));;\
\
    # 2. DER BEFEHLS-LOOP\
    foreach my $cmd_part (@commands) {\
        \
        $cmd_part =~ s/^\s+|\s+$//g;;\
        \
        my $is_on  = ($cmd_part =~ /$onRegEx/) ? 1 : 0;;\
        my $is_off = ($cmd_part =~ /$offRegEx/) ? 1 : 0;;\
\
        $cmd_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der)\b//g;;\
\
        # --- INTENT: STAUBSAUGER ---\
        if ($cmd_part =~ /(reinige|sauge|putze|staubsauger|roboter)/) {\
            my @found = grep { $cmd_part =~ /\b$_\b/ } keys %vacRooms;;\
            if (@found) {\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish clean_segment " . join(",", map { $vacRooms{$_} } @found));;\
            } else {\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish start");;\
            }\
            next;;\
        } elsif ($cmd_part =~ /(lade|aufladen|dock|station|home)/) {\
            fhem("set MQTT2_valetudo_FlusteredUnequaledFish charge");;\
            next;;\
        }\
\
        # --- INTENT: FERNSEHER ---\
        if ($cmd_part =~ /(fernseher|tv|vuplus)/) {\
            fhem("set VuPlus " . ($is_off ? "off" : "on"));;\
            next;;\
        }\
\
        # --- INTENT: AMBIENTE ---\
        if ($cmd_part =~ /ambiente/) {\
            if ($cmd_part =~ /(\d+)/) {\
                my $b = ($1 > 255 ? 255 : ($1 < 1 ? 1 : $1));;\
                fhem("set LampeSzeneAlle brightness $b");;\
            } else {\
                fhem("set LampeSzeneAlle " . ($is_off ? "off" : "on"));;\
            }\
            next;;\
        }\
\
        # --- INTENT: AMBILIGHT ---\
        if ($cmd_part =~ /ambilight/) {\
            system("sshpass -p 'GEHEIMESPASSWORT' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\
            next;;\
        }\
\
        # --- INTENT: LICHT ---\
        my ($lightRoom) = grep { $cmd_part =~ /\b$_\b/ } keys %lightRooms;;\
        if ($lightRoom || $cmd_part =~ /(licht|lampe)/) {\
            my $dev = $lightRooms{$lightRoom}{dev} // "LampeSzeneAlle";;\
            fhem("set $dev " . ($is_off ? "off" : "on")) if ($is_on || $is_off);;\
            next;;\
        }\
\
        # --- HILFE (DYNAMISCH) ---\
        if ($cmd_part =~ /(hilfe|kommandos|übersicht)/) {\
\
            my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;">';;\
            $h .= '<b>Befehlsübersicht:</b><br><br>';;\
\
            # Licht\
            $h .= '<u>Licht</u><br>';;\
            for my $k (sort keys %lightRooms) {\
                $h .= "• ".$lightRooms{$k}{label}." an/aus<br>";;\
            }\
\
            # Staubsauger\
            $h .= '<br><u>Staubsauger</u><br>';;\
            $h .= "• Sauge [Raum]<br>";;\
            $h .= "  Räume: ".join(", ", map { ucfirst($_) } sort keys %vacRooms)."<br>";;\
            $h .= "• Lade Roberto<br>";;\
\
            # Sonstiges\
            $h .= '<br><u>Sonstiges</u><br>';;\
            $h .= "• Fernseher an/aus<br>";;\
            $h .= "• Ambiente [an|aus|1-255]<br>";;\
            $h .= "• Ambilight<br>";;\
\
            $h .= '</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");;\
\
            next;;\
        }\
    }\
}


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
  • FireOS:
  „Tastatur und Sprache“ → „Text-to-Speech“
  • Fully Browser:
  Enable JavaScriptInterface (PLUS) aktivieren