Schau Dir mal die Utils.pm von HTTPMOD an
https://svn.fhem.de/trac/browser/trunk/fhem/lib/FHEM/HTTPMOD/Utils.pm
Da gibt es einige Hilfsfunktionen bezüglich Timer.
Das geht schon mal in die richtige Richtung, das "Problem" ist da nur, dass die Namen der Timer starr sind ("update"). Würde statt dieses starren Namens eine Option bestehen, einen eigenen Namen zu übergeben, könnte man das auch für RHASSPY wohl mit diesen Funktionen lösen. (Für WeekdayTimer und Twilight ginge es evtl. auch so schon).
(OT: Fehlt in HTTPMOD nicht eine RenameFn? MAn. müsste es Probleme geben, den $hash der ursprünglichen Instanz zu ermitteln, wenn GetUpdate nach Namensänderung der zugehörigen Instanz aufgerufen wird. Ich habe das aber nicht vertieft, vermutlich übersehe ich was.
Die Routinen in RHASSPY umgehen das Problem, indem gleich in das übergebene Argument auch der Instanz-Hash mit übergeben wird.)
Ich verstehe noch nicht das Problem, was hier geloest werden soll.
Bitte entschuldige, wenn ich das Problem bisher nicht hinreichend klar beschrieben habe.
Folgende Situation:
Wir haben uU. (mind.) zwei "Mikrofone", die je unabhängig voneinander einen Dialog zu einem externen Dienst aufmachen. Das kann mehr oder weniger gleichzeitig geschehen. Der Dienst vergibt dann jeweils eine eigene "SessionId", analysiert die jeweiligen Daten und schickt dann die aufbereiteten Daten in einem JSON-Blob an einen MQTT-Server, auf den dann wieder andere Dienste lauschen, u.A. eben FHEM mit Hilfe des Moduls RHASSPY. Die SessionId gilt nur für den Zeitraum zwischen Start eines Dialogs und dessen Beendigung, der Dialog selbst kann aber von allen Teilnehmern am MQTT-Verkehr (eine zeitlang) offen gehalten werden.
Ergo kann es in unserem Beispiel mit den zwei "Mikrofonen" dazu kommen, dass eine RHASSPY-Instanz parallel mit zwei SessionId's umgehen muss, um z.B. von beiden Mikrofonen zu erfahren, welche genaue Anweisung geben werden soll (weil mehrere passen, aber mit einiger Wahrscheinlichkeit nur eine ausgeführt werden soll, oder eine Anweisung z.B. nur ausgeführt werden soll, wenn das ausdrücklich bestätigt wird).
Kommt dann aber weder eine Bestätigung noch eine Auswahl, muss "jemand" irgendwann den Dialog wieder zu machen (und ggf. die Grundeinstellungen für Dialoge mit diesem "Mikrofon" wieder herstellen). Im Moment wird das dadurch erledigt, dass eben für jeden Dialog ein eigener InternalTimer mit eigener "Kennung" angelegt wird.
Die Alternative, auf die ich jetzt wärend des Nachdenkens über das Thema gekommen bin, wäre ggf. die, im Modul ein Array mit den SessionId's (und den zugehörigen Ablaufdaten) vorzuhalten, und dann eben anhand der Ablaufdaten beim Aufruf der timeout-Funktion zu checken, welche SessionId's bereits abgelaufen sind bzw. wann dann der nächste Aufruf erfolgen soll (ähnlich structure_asyncQueue), falls noch was aussteht.
Hätte den Vorteil, dass das zentrale Timer-Array kleiner gehalten wird, aber den Nachteil, dass man beim Setzen eines neuen Timers im Modul dann aufpassen muss, dass man keinen bereits laufenden (mit früherem Endezeitpunkt) löscht. Hat halt alles seine Vor- und Nachteile...
Hier jedenfalls mal der runtergestrippte Code von RHASSPY, vielleicht trägt das zur weiteren Klärung bei (der ist für sich genommen so nicht lauffähig).
# Device löschen
sub Undefine {
my $hash = shift // return;
deleteAllRegisteredInternalTimer($hash);
RemoveInternalTimer($hash);
return;
}
sub RHASSPY_DialogTimeout {
my $fnHash = shift // return;
my $hash = $fnHash->{HASH} // $fnHash;
return if (!defined($hash));
my $identiy = $fnHash->{MODIFIER};
my $data = shift // $hash->{helper}{'.delayed'}->{$identiy};
delete $hash->{helper}{'.delayed'}{$identiy};
deleteSingleRegisteredInternalTimer($identiy, $hash);
my $siteId = $data->{siteId};
my $toDisable = defined $data{'.ENABLED'} ? $data->{'.ENABLED'} : [qw(ConfirmAction CancelAction)];
my $response = $hash->{helper}{lng}->{responses}->{DefaultConfirmationTimeout};
respond ($hash, $data->{requestType}, $data->{sessionId}, $siteId, $response);
configure_DialogManager($hash, $siteId, $toDisable, 'false');
return;
}
sub setDialogTimeout {
my $hash = shift // return;
my $data = shift // return; # $hash->{helper}{'.delayed'};
my $timeout = shift;
my $response = shift;
my $toEnable = shift // [qw(ConfirmAction CancelAction)];
my $siteId = $data->{siteId};
$data->{'.ENABLED'} = $toEnable;
my $identiy = qq($data->{sessionId});
$response = $hash->{helper}{lng}->{responses}->{DefaultConfirmationReceived} if $response eq 'default';
$hash->{helper}{'.delayed'}{$identiy} = $data;
resetRegisteredInternalTimer( $identiy, time + $timeout, \&RHASSPY_DialogTimeout, $hash, 0);
#InternalTimer(time + $timeout, \&RHASSPY_DialogTimeout, $hash, 0);
#[...]
return; # $toTrigger;
}
# Eingehende "GetNumeric" Intents bearbeiten
sub handleIntentGetNumeric {
my $hash = shift // return;
my $data = shift // return;
my $value;
Log3($hash->{NAME}, 5, "handleIntentGetNumeric called");
# Mindestens Type oder Device muss existieren
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultError')) if !exists $data->{Type} && !exists $data->{Device};
my $type = $data->{Type};
my $subType = $data->{subType} // $type;
my $room = getRoomName($hash, $data);
# Get suitable device
my $device = exists $data->{Device}
? getDeviceByName($hash, $room, $data->{Device})
: getDeviceByIntentAndType($hash, $room, 'GetNumeric', $type)
// return respond($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound'));
#more than one device
if (ref $device eq 'ARRAY') {
#until now: only extended test code
my $first = $device->[0];
my $response = $device->[1];
my $all = $device->[2];
my $choice = $device->[3];
my $toActivate = $choice eq 'RequestChoiceDevice' ? [qw(ChoiceDevice CancelAction)] : [qw(ChoiceRoom CancelAction)];
$device = $first;
Log3($hash->{NAME}, 5, "More than one device possible, response is $response, first is $first, all are $all, type is $choice");
return setDialogTimeout($hash, $data, 20, $response, $toActivate);
}
#[...]
# Antwort senden
return; # respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
}
sub handleIntentCancelAction {
my $hash = shift // return;
my $data = shift // return;
Log3($hash->{NAME}, 5, 'handleIntentCancelAction called');
my $toDisable = defined $data->{customData} && defined $data->{customData}->{'.ENABLED'} ? $data->{customData}->{'.ENABLED'} : [qw(ConfirmAction CancelAction)];
my $response = $hash->{helper}{lng}->{responses}->{ 'SilentCancelConfirmation' };
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response) if !defined $data->{customData};
#might lead to problems, if there's more than one timeout running...
#RemoveInternalTimer( $hash, \&RHASSPY_DialogTimeout );
my $identiy = qq($data->{sessionId});
deleteSingleRegisteredInternalTimer($identiy, $hash);
$response = $hash->{helper}{lng}->{responses}->{ 'DefaultCancelConfirmation' };
configure_DialogManager($hash, $data->{siteId}, $toDisable, 'false');
return $hash->{NAME};
}
sub handleIntentConfirmAction {
my $hash = shift // return;
my $data = shift // return;
Log3($hash->{NAME}, 5, 'handleIntentConfirmAction called');
#cancellation case
#return RHASSPY_DialogTimeout($hash, 1, $data) if $data->{Mode} ne 'OK';
return handleIntentCancelAction($hash, $data) if $data->{Mode} ne 'OK';
#confirmed case
my $identiy = qq($data->{sessionId});
my $data_saved = $hash->{helper}{'.delayed'}->{$identiy};
delete $hash->{helper}{'.delayed'}{$identiy};
deleteSingleRegisteredInternalTimer($identiy, $hash);
my $data_old = $data_saved;
my $toDisable = defined $data_old && defined $data_old->{'.ENABLED'} ? $data_old->{'.ENABLED'} : [qw(ConfirmAction CancelAction)];
configure_DialogManager($hash, $data->{siteId}, $toDisable, 'false');
return respond( $hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse( $hash, 'DefaultConfirmationNoOutstanding' ) ) if ! defined $data_old;
$data_old->{siteId} = $data->{siteId};
$data_old->{sessionId} = $data->{sessionId};
$data_old->{requestType} = $data->{requestType};
$data_old->{Confirmation} = 1;
my $intent = $data_old->{intent};
my $device = $hash->{NAME};
# Passenden Intent-Handler aufrufen
if (ref $dispatchFns->{$intent} eq 'CODE') {
$device = $dispatchFns->{$intent}->($hash, $data_old);
}
return $device;
}
sub handleIntentChoiceDevice {
my $hash = shift // return;
my $data = shift // return;
Log3($hash->{NAME}, 5, 'handleIntentChoiceDevice called');
#my $data_old = $data->{customData};
my $identiy = qq($data->{sessionId});
my $data_old = $hash->{helper}{'.delayed'}->{$identiy};
delete $hash->{helper}{'.delayed'}{$identiy};
deleteSingleRegisteredInternalTimer($identiy, $hash);
return respond( $hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse( $hash, 'DefaultChoiceNoOutstanding' ) ) if ! defined $data_old;
$data_old->{siteId} = $data->{siteId};
$data_old->{sessionId} = $data->{sessionId};
$data_old->{requestType} = $data->{requestType};
$data_old->{Device} = $data->{Device};
my $intent = $data_old->{intent};
my $device = $hash->{NAME};
# Passenden Intent-Handler aufrufen
if (ref $dispatchFns->{$intent} eq 'CODE') {
$device = $dispatchFns->{$intent}->($hash, $data_old);
}
return $device;
}
# borrowed from WeekdayTimer
################################################################################
sub resetRegisteredInternalTimer {
my ( $modifier, $tim, $callback, $hash, $initFlag ) = @_;
deleteSingleRegisteredInternalTimer( $modifier, $hash, $callback );
return setRegisteredInternalTimer ( $modifier, $tim, $callback, $hash, $initFlag );
}
################################################################################
sub setRegisteredInternalTimer {
my $modifier = shift // return;
my $tim = shift // return;
my $callback = shift // return;
my $hash = shift // return;
my $initFlag = shift // 0;
my $timerName = "$hash->{NAME}_$modifier";
my $fnHash = {
HASH => $hash,
NAME => $timerName,
MODIFIER => $modifier
};
if ( defined( $hash->{TIMER}{$timerName} ) ) {
Log3( $hash, 1, "[$hash->{NAME}] possible overwriting of timer $timerName - please delete it first" );
stacktrace();
}
else {
$hash->{TIMER}{$timerName} = $fnHash;
}
Log3( $hash, 5, "[$hash->{NAME}] setting Timer: $timerName " . FmtDateTime($tim) );
InternalTimer( $tim, $callback, $fnHash, $initFlag );
return $fnHash;
}
################################################################################
sub deleteSingleRegisteredInternalTimer {
my $modifier = shift;
my $hash = shift // return;
my $callback = shift;
my $timerName = "$hash->{NAME}_$modifier";
my $fnHash = $hash->{TIMER}{$timerName};
if ( defined($fnHash) ) {
Log3( $hash, 5, "[$hash->{NAME}] removing Timer: $timerName" );
RemoveInternalTimer($fnHash);
delete $hash->{TIMER}{$timerName};
}
return;
}
################################################################################
sub deleteAllRegisteredInternalTimer {
my $hash = shift // return;
for my $key ( keys %{ $hash->{TIMER} } ) {
deleteSingleRegisteredInternalTimer( $hash->{TIMER}{$key}{MODIFIER}, $hash );
}
return;
}
1;
__END__