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 191: Zeile 191:
Beide Wege schreiben in:
Beide Wege schreiben in:


<code>global:STT</code>
<code>global:STT_output</code>


Die Verarbeitung erfolgt zentral über ein <code>notify</code>. Ein Device wird in der Mappingtabelle eingetragen.
Die Verarbeitung erfolgt zentral über ein <code>notify</code>. Ein Device wird in der Mappingtabelle eingetragen.
Zeile 201: Zeile 201:
<code>"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname",  cmdOn => "on", cmdOff => "off" }</code>
<code>"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname",  cmdOn => "on", cmdOff => "off" }</code>


=== Beispiel: notify ===
=== Beispiel: notify zur Steuerung ===


<syntaxhighlight lang="perl">
<syntaxhighlight lang="perl">
defmod n_VoiceControl notify global:STT:.* {\
defmod n_VoiceControl notify global:STT_output:.* {\
    # --- VORBEREITUNG ---\
     my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
     my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
     $cleanEvent //= $EVENT;;\
     $cleanEvent //= $EVENT;;\
     $clientId  //= "unknown";;\
     $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/;;\
\
\
     # --- KONFIGURATION: Zentrales Mapping ---\
     # --- 1. Geräte-Liste ---\
     my %smartHomeDevices = (\
     my %devices = (\
         "esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer",  cmdOn => "on", cmdOff => "off" },\
         "esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer (an/aus)" },\
         "esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium", cmdOn => "on", cmdOff => "off" },\
         "esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium (an/aus)" },\
         "küche" => { dev => "Deckenlampe_Kue", label => "Licht Küche", cmdOn => "on", cmdOff => "off" },\
        "küche:licht|lampe|deckenlampe" => { dev => "Deckenlampe_Kue", label => "Licht Küche (an/aus)" },\
         "wohnzimmer" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer", cmdOn => "on", cmdOff => "off" },\
         "küche:radio" => { dev => "MPD", label => "Radio Küche(an/aus)", cmdOn => "play", cmdOff => "stop" },\
         "fernseher|tv" => { dev => "VuPlus", label => "Fernseher", cmdOn => "on", cmdOff => "off" },\
         "wohnzimmer:licht|lampe|deckenlampe" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer (an/aus)" },\
         "rechner|pc" => { dev => "PC_Aktor", label => "PC", cmdOn => "on", cmdOff => "off" },\
         "fernseher|tv" => { dev => "VuPlus", label => "Fernseher (an/aus)" },\
         "garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor öffnen/schließen", cmdOn => "open", cmdOff => "close" },\
         "rechner|pc" => { dev => "PC_Aktor", label => "PC (an/aus)" },\
         "kaffee" => { dev => "Kaffeemaschine", label => "Kaffeemaschine", cmdOn => "on", cmdOff => "off" },\
         "garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor (öffnen/schließen)", cmdOn => "open", cmdOff => "close" },\
         "roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto", cmdOn => "charge" },\
         "kaffee" => { dev => "Kaffeemaschine (an/aus)", label => "Kaffeemaschine" },\
         "ambiente" => { dev => "LampeSzeneAlle", label => "Zentrales Licht", type => "dimmer", cmdOn => "on", cmdOff => "off" },\
         "roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto (an/aus)", cmdOn => "charge" },\
         "sauge|reinige|putze|staubsauger|roboter" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Staubsauger", type => "vacuum" },\
        \
        "ambilight" => { label => "Ambilight", type => "system", cmd => "sshpass -p 'GEHEIMESPASSWORD' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'" }\
        # 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";;\
        }}\
     );;\
     );;\
\
\
     my %vacRooms = ("arbeitszimmer" => "Arbeitszimmer", "badezimmer" => "Badezimmer", "esszimmer" => "Esszimmer", "flur" => "Flur", "küche" => "Küche", "wohnzimmer" => "Wohnzimmer");;\
    # --- 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 $onRegEx  = '\b(an|ein|einschalten|starte|aktivier|aktiviere|öffne|öffnen|auf|hoch|lade)\b';;\
     my @parts = split(/\s*(?:dann|,)\s*|(?<=\ban\b)\s*und\s*|(?<=\baus\b)\s*und\s*/, $text);;\
     my $offRegEx = '\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere|schließe|schließen|zu|runter)\b';;\
     @parts = ($text) if @parts == 1;;\
\
\
     my @commands = split(/\s*(?:und|dann|,)\s*/, lc($cleanEvent));;\
     for my $part (@parts) {\
        $part =~ s/^\s+|\s+$//g;;\
        next unless $part;;\
\
\
    # --- DER BEFEHLS-LOOP ---\
        my $is_on  = $part =~ /$onRegEx/ ? 1 : ($part =~ /$offRegEx/ ? 0 : $global_on);;\
    foreach my $cmd_part (@commands) {\
         my $is_off = $part =~ /$offRegEx/ ? 1 : ($part =~ /$onRegEx/ ? 0 : $global_off);;\
        $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;;\
\
\
         # --- GENERISCHE STEUERUNG ---\
         # Trennung: Original-Part bleibt für "run"-Blöcke, clean_part kriegt die Füllwortbereinigung\
         my $match = 0;;\
         my $clean_part = $part;;\
         # Sortierung stellt sicher, dass 'dimmer/vacuum' Vorrang haben\
         $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;;\
         foreach my $key (sort { ($smartHomeDevices{$b}{type}//"") cmp ($smartHomeDevices{$a}{type}//"") } keys %smartHomeDevices) {\
\
            \
         # 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);;\
             my ($main, $must) = split(/:/, $key);;\
             \
             \
             # Prüfe Hauptwort\
             if ($part =~ /\b($main)\b/ || $clean_part =~ /\b($main)\b/) {\
            if ($cmd_part =~ /\b($main)\b/i) {\
                 next if $must && $part !~ /\b($must)\b/;;\
                # Prüfe Pflichtwort falls vorhanden\
                 next if ($must && $cmd_part !~ /\b($must)\b/i);;\
                \
                my $d = $smartHomeDevices{$key};;\
\
\
                 # A) System\
                 my $d = $devices{$key};;\
                if (($d->{type} // "") eq "system") {\
                 if ($d->{run}) {\
                    system($d->{cmd});;\
                     my $res = $d->{run}->($d, $part, $is_on, $is_off);;\
                }\
                    push @responses, $res if $res;;\
                \
                } elsif ($is_on || $is_off) {\
                # B) Staubsauger\
                 if (($d->{type} // "") eq "vacuum") {\
                     my @found = grep { $cmd_part =~ /\b$_\b/ } keys %vacRooms;;\
                    fhem(@found ? "set $d->{dev} clean_segment ".join(",", map { $vacRooms{$_} } @found) : "set $d->{dev} start");;\
                }\
                # C) Dimmer (Kompakt & korrigiert)\
                elsif (($d->{type} // "") eq "dimmer") {\
                    $cmd_part =~ /(\d+)/ \
                        ? fhem("set $d->{dev} brightness " . int(($1 > 100 ? 100 : $1) * 2.55)) \
                        : ($is_on || $is_off) && fhem("set $d->{dev} " . ($is_off ? "off" : "on"));;\
                }\
                # D) Standard An/Aus\
                else {\
                     my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\
                     my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\
                     fhem("set $d->{dev} $fhem_cmd") if ($is_on || $is_off);;\
                     fhem("set $d->{dev} $fhem_cmd");;\
                    push @responses, "$d->{label} " . ($is_off ? "aus" : "an");;\
                 }\
                 }\
                \
                $match = 1;; last;;\
             }\
             }\
         }\
         }\
        next if $match;;\
    }\
\
\
        # --- HILFE ---\
    # --- 5. FINALE SPRACHAUSGABE ---\
        if ($cmd_part =~ /(hilfe|kommandos|übersicht)/) {\
    fhem("sleep 2;; setreading global TTS_input erledigt") if @responses;;\
            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 %smartHomeDevices) {\
                my $d = $smartHomeDevices{$k};;\
                next if $seen{$d->{label}}++;; \
                my $suffix = ($d->{cmdOn} && $d->{cmdOff} && $d->{label} !~ /(an\/aus|öffnen\/schließen)/i) ? " (an/aus)" : "";;\
                $suffix = " [0-100%]" if ($d->{type} // "") eq "dimmer";;\
                $h .= "• $d->{label}$suffix<br>";;\
            }\
            $h .= '<br><u>Staubsauger Räume</u><br>• '.join(", ", map { ucfirst($_) } sort keys %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");;\
        }\
    }\
}
}
</syntaxhighlight>
</syntaxhighlight>


=== Beispiel: notify für Sprachausgabe mit dem TTS-Modul ===
<syntaxhighlight lang="perl">
defmod n_global_TTS_output notify global:TTS_output:.* { my $text = $EVENT;;;; $text =~ s/^TTS_output:\s*//;;;; fhem("set TTS tts $text") }
</syntaxhighlight>


== Unterschiede der Wege ==
== Unterschiede der Wege ==

Aktuelle Version vom 31. Mai 2026, 11:28 Uhr

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


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 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 Enabled setzen


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

  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

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 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.45   

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“

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