From 39dcc7e77f0735dcfaf9423afe2e1b73c31d8d15 Mon Sep 17 00:00:00 2001 From: Beta-User <> Date: Fri, 18 Oct 2024 15:16:05 +0000 Subject: [PATCH] 10_RHASSPY: add resetInput option; #139337 git-svn-id: https://svn.fhem.de/fhem/trunk@29261 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/10_RHASSPY.pm | 100 ++++++++++++------ fhem/contrib/RHASSPY/99_RHASSPY_Utils_Demo.pm | 7 +- fhem/contrib/RHASSPY/rhasspy-de.cfg | 7 ++ 3 files changed, 82 insertions(+), 32 deletions(-) diff --git a/fhem/FHEM/10_RHASSPY.pm b/fhem/FHEM/10_RHASSPY.pm index 5295d96af..a75959b5d 100644 --- a/fhem/FHEM/10_RHASSPY.pm +++ b/fhem/FHEM/10_RHASSPY.pm @@ -103,7 +103,8 @@ my $languagevars = { 'DefaultConfirmationBack' => "So once more.", 'DefaultConfirmationTimeout' => "Sorry, too late to confirm.", 'DefaultCancelConfirmation' => "Thanks, aborted.", - 'SilentCancelConfirmation' => "", + 'RetryIntent' => "Please try again", + 'SilentClosure' => " ", 'DefaultConfirmationReceived' => "Ok, will do it!", 'DefaultConfirmationNoOutstanding' => "No command is awaiting confirmation!", 'DefaultConfirmationRequestRawInput' => 'Please confirm: $rawInput!', @@ -1696,7 +1697,7 @@ sub RHASSPY_DialogTimeout { deleteSingleRegIntTimer($identity, $hash, 1); - respond( $hash, $data, getResponse( $hash, 'DefaultConfirmationTimeout' ) ); + respond( $hash, $data, getResponse( $hash, defined $data->{silent} ? 'SilentClosure' : 'DefaultConfirmationTimeout' ) ); delete $hash->{helper}{'.delayed'}{$identity}; return; @@ -2845,7 +2846,6 @@ sub Parse { Log3($hash,5,"RHASSPY: [$hash->{NAME}] Parse (IO: ${ioname}): Msg: $topic => $value"); $data //= parseJSONPayload($hash, $value); #Beta-User: Calling parseJSONPayload() only once should be ok, as there's no code-page dependency any longer - #my $fret = analyzeMQTTmessage($hash, $topic, $value); my $fret = analyzeMQTTmessage($hash, $topic, $value, $data); next if !defined $fret; if( ref $fret eq 'ARRAY' ) { @@ -3475,18 +3475,21 @@ sub analyzeMQTTmessage { my $message = shift;# // carp q[No message provided!] && return;; my $data = shift;# // carp q[No message provided!] && return;; - #my $data = parseJSONPayload($hash, $message); my $fhemId = $hash->{fhemId}; + my $name = $hash->{NAME}; my $input = $data->{input}; my $device; - my @updatedList; + my @updatedList = $hash->{NAME}; my $type = $data->{type} // q{text}; my $sessionId = $data->{sessionId}; my $siteId = $data->{siteId}; my $mute = 0; + if (!defined $data->{requestType}) { + $data->{requestType} = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice'; + } if (defined $siteId) { my $reading = makeReadingName($siteId); @@ -3519,7 +3522,6 @@ sub analyzeMQTTmessage { deleteSingleRegIntTimer($identity, $hash); } } - push @updatedList, $hash->{NAME}; return \@updatedList; } # Hotword detection @@ -3530,8 +3532,10 @@ sub analyzeMQTTmessage { readingsSingleUpdate($hash, "hotwordAwaiting_" . makeReadingName($siteId), $active, 1); my $ret = handleHotwordGlobal($hash, $active ? 'on' : 'off', $data, $active ? 'on' : 'off'); - push @updatedList, $ret if $ret && $defs{$ret}; - push @updatedList, $hash->{NAME}; + my @candidates = ref $ret eq 'ARRAY' ? $ret : split m{,}x, $ret; + for (@candidates) { + push @updatedList, $_ if $defs{$_} && $_ ne $name; + } return \@updatedList; } @@ -3543,7 +3547,6 @@ sub analyzeMQTTmessage { # update Readings updateLastIntentReadings($hash, $topic,$data); handleIntentSetMute($hash, $data); - push @updatedList, $hash->{NAME}; return \@updatedList; } @@ -3559,30 +3562,45 @@ sub analyzeMQTTmessage { push @updatedList, $device; } } - return \@updatedList if !$hash->{handleHotword} && !defined $hash->{helper}{hotwords}; + #return \@updatedList if !$hash->{handleHotword} && !defined $hash->{helper}{hotwords}; + return if !$hash->{handleHotword} && !defined $hash->{helper}{hotwords}; my $ret = handleHotwordDetection($hash, $hotword, $data); - push @updatedList, $ret if $ret && $defs{$ret}; + my @candidates = ref $ret eq 'ARRAY' ? $ret : split m{,}x, $ret; + for (@candidates) { + push @updatedList, $_ if $defs{$_} && $_ ne $name; + } $ret = handleHotwordGlobal($hash, $hotword, $data, 'detected'); - push @updatedList, $ret if $ret && $defs{$ret}; - push @updatedList, $hash->{NAME}; + @candidates = ref $ret eq 'ARRAY' ? $ret : split m{,}x, $ret; + for (@candidates) { + push @updatedList, $_ if $defs{$_} && $_ ne $name; + } return \@updatedList; } if ( $topic =~ m{\Ahermes/tts/say}x ) { return if !$hash->{siteId} || $siteId ne $hash->{siteId}; my $ret = handleTtsMsgDialog($hash, $data); - push @updatedList, $ret if $ret && $defs{$ret}; - push @updatedList, $hash->{NAME}; + my @candidates = ref $ret eq 'ARRAY' ? $ret : split m{,}x, $ret; + for (@candidates) { + push @updatedList, $_ if $defs{$_} && $_ ne $name; + } return \@updatedList; } if ($mute) { - $data->{requestType} = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice'; - respond( $hash, $data, q{ }, 'endSession', 0 ); + #$data->{requestType} = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice'; + respond( $hash, $data, getResponse( $hash, 'SilentClosure' ), 'endSession', 0 ); #Beta-User: Da fehlt mir der Soll-Ablauf für das "room-listening"-Reading; das wird ja über einen anderen Topic abgewickelt return \@updatedList; } + #see https://forum.fhem.de/index.php?msg=1322195 for details + if ( defined $data->{resetInput} && defined $data->{siteId}) { + handleIntentCancelAction($hash, $data); + activateVoiceInput($hash,[$data->{siteId}]) if $data->{requestType} eq 'voice'; + return \@updatedList; + } + if ($topic =~ m{\Ahermes/nlu/intentNotRecognized}x && defined $siteId) { return testmode_parse($hash, 'intentNotRecognized', $data) if defined $hash->{testline} && defined $hash->{siteId} && $siteId eq $hash->{siteId}; return handleIntentNotRecognized($hash, $data); @@ -3607,12 +3625,10 @@ sub analyzeMQTTmessage { $device = handleCustomIntent($hash, $intent, $data); } - my $name = $hash->{NAME}; $device = $device // $name; - $device .= ",$name" if $device !~ m{$name}x; my @candidates = split m{,}x, $device; for (@candidates) { - push @updatedList, $_ if $defs{$_}; + push @updatedList, $_ if $defs{$_} && $_ ne $name; } Log3($hash, 4, "[$name] dispatch result is @updatedList" ); @@ -4218,12 +4234,13 @@ sub handleCustomIntent { $response = ${$error}[0] // getResponse($hash, 'DefaultConfirmation'); if ( ref ${$error}[0] eq 'HASH') { $timeout = ${$error}[1] if looks_like_number( ${$error}[1] ); + $timeout = $error->{sessionTimeout} if defined $error->{sessionTimeout} && looks_like_number( $error->{sessionTimeout} ); return setDialogTimeout($hash, $data, $timeout, ${$error}[0]); } respond( $hash, $data, $response ); return ${$error}[1]; #comma separated list of devices to trigger } elsif ( ref $error eq 'HASH' ) { - $timeout = $error->{sessionTimeout} if defined $error->{sessionTimeout} && looks_like_number( $error->{sessionTimeout} ); + $timeout = $error->{sessionTimeout} if defined $error->{sessionTimeout} && looks_like_number( $error->{sessionTimeout} ); return setDialogTimeout($hash, $data, $timeout, $error); } else { $response = $error; # if $error && $error !~ m{Please.define.*first}x; @@ -5671,7 +5688,29 @@ sub handleIntentNotRecognized { $data->{requestType} = 'text'; return respond( $hash, $data, getResponse( $hash, 'NoIntentRecognized' )); } - return; #Beta-User: End of recent changes... + #return; #Beta-User: End of recent changes... + + # @@@ added by GV start - THIS keeps the session open speaking "please try again" after IntentNotRecognized - can repeatedly happen, but terminates with dialogue timeout + $data->{requestType} //= $data_old->{requestType} // 'voice'; # required, otherwise session open but no voice input possible + my $intentFilter = 'null'; #don't disable anything by default + $intentFilter = $data_old->{'.ENABLED'} if defined $data_old->{'.ENABLED'} && ref $data_old->{'.ENABLED'} eq 'ARRAY'; # get intent filter(s) + my @ca_strings; # just copied from setDialogTimeout + $intentFilter = split m{,}xms, $intentFilter if ref $intentFilter ne 'ARRAY'; #Beta-User: Das ist an dieser Stelle nicht logisch... + if (ref $intentFilter eq 'ARRAY') { + for (@{$intentFilter}) { + my $id = qq{$hash->{LANGUAGE}.$hash->{fhemId}:$_}; + push @ca_strings, $id; + } + } + my $response = getResponse( $hash, 'RetryIntent'); # get retry response + my $reaction = { text => $response, # hash to control respond() behaviour + intentFilter => [@ca_strings], + sendIntentNotRecognized => 'true', #'false', + customData => $data->{customData} + }; + respond( $hash, $data, $reaction); # keep session open and continue listening + return $hash->{NAME}; +# @@@ added by GV end =pod return if !defined $data->{input} || length($data->{input}) < 12; #Beta-User: silence chuncks or single words, might later be configurable @@ -5696,8 +5735,8 @@ sub handleIntentCancelAction { my $identity = qq($data->{sessionId}); my $data_old = $hash->{helper}{'.delayed'}->{$identity}; - if ( !defined $data_old ) { - respond( $hash, $data, getResponse( $hash, 'SilentCancelConfirmation' ), undef, 0 ); + if ( !defined $data_old || defined $data->{resetInput}) { + respond( $hash, $data, getResponse( $hash, 'SilentClosure' ), undef, 0 ); return configure_DialogManager( $hash, $data->{siteId}, undef, undef, 1 ); #global intent filter seems to be not working! } @@ -6200,7 +6239,7 @@ So all parameters in define should be provided in the key=value form. In
encoding=cp-1252
. Do not set this unless you experience encoding problems!RHASSPY needs a MQTT2_CLIENT device connected to the same MQTT-Server as the voice assistant (Rhasspy) service.
@@ -6405,11 +6444,11 @@ After changing something relevant within FHEM for either the data structure inIf a simple text is returned, this will be considered as response, if return value is not defined, the default response will be used.
- For more advanced use of this feature, you may return either a HASH or an ARRAY data structure. If ARRAY is returned:
+ For more advanced use of this feature, you may return either a HASH or an ARRAY data structure (Note: The formating of the required data might be subject to changes!). If ARRAY is returned:
Atm. Rhasspy will activate all known intents at startup. As some of the intents used by FHEM are only needed in case some dialogue is open, it will deactivate these intents (atm: ConfirmAction, CancelAction, ChoiceRoom and ChoiceDevice(including the additional parts derived from language and fhemId))) at startup or when no active filtering is detected. You may disable additional intents by just adding their names in intentFilter line or using an explicit state assignment in the form intentname=true (Note: activating the 4 mentionned intents is not possible!). For details on how configure works see Rhasspy documentation.
+Atm. Rhasspy will activate all known intents at startup. As some of the intents used by FHEM are only needed in case some dialogue is open, it will deactivate these intents (atm: ConfirmAction, CancelAction, Choice, ChoiceRoom and ChoiceDevice(including the additional parts derived from language and fhemId))) at startup or when no active filtering is detected. You may disable additional intents by just adding their names in intentFilter line or using an explicit state assignment in the form intentname=true (Note: activating the 4 mentionned intents is not possible!). For details on how configure works see Rhasspy documentation.
The following intents are directly implemented in RHASSPY code and the keywords used by them in sentences.ini are as follows: +
The following intents are directly implemented in RHASSPY code. Note: In case you hand over a key named "resetInput", no specific intent handler will be called, but the respective satellite will be activated and reset to it's default intentFilter.
+The keywords used by these intents (in sentences.ini) are as follows:
attr <rhasspyDevice> rhasspyIntents SetCustomIntentsDataTest=RHASSPY::Demo::DataTest(NAME,DATA)
attr <rhasspyDevice> rhasspyIntents SetCustomIntentsDialogueTest=RHASSPY::Demo::DialogueTest(NAME,DATA)