Asterisk: CDR im hangup-Kontext
in Linux, PHP | Tags: Asterisk, Call Data Record, CDR, hangup, Kontext, PHP, ZeitstempelSowie ich heute recherchiert habe, ist es bis heute bei allen Asterisk-Versionen gleich geblieben: Man kann innerhalb des h-Kontextes (“after hangup”) noch nicht auf die Felder des Call Data Records (CDR) zugreifen. Das bedeutet ganz konkret, dass sich innerhalb der Abarbeitung eigener Anrufskripte in /etc/asterisk/extensions.conf kaum zur Laufzeit eines Anrufs sinnvoll abrechnen lässt. Doch es gibt einen kleinen Trick, dies auszuhebeln…
Asterisk und der Call Data Record
Normalerweise geht man als Benutzer einer Telefonsoftware davon aus, dass nach dem Auflegen gewisse Daten eines Anrufs verfügbar sind: Woher der Anrufer kam, wer den Anruf entgegennahm und wie lange der Anruf dauerte. Vor allem die Anrufdauer ab dem Antworten ist besonders interessant, da es die abzurechnende Telefonzeit ist und oft mit Kosten verbunden.
Asterisk schreibt den CDR, wenn richtig konfiguriert, in eine Datenbank. Das ist sehr schön, und man kann die Daten auch sehr gut am Ende des Tages oder Monats abrechnen. Möchte man allerdings die Daten bereits am Ende eines Gesprächs benutzen, so sitzt man in der Tinte: Erst nach kompletter Abarbeitung aller Kontexte erstellt Asterisk die CDR-Daten wie Dauer ab klingen in CDR(duration), Dauer des Telefonats CDR(billsec) et cetera.
Es gab Versuche, den internen Asterisk-Code für diese Abrechnungszwecke umzuschachteln, also den CDR nach Abbau der Bridge zu schreiben, und dann erst in den h-Kontext (die Abarbeitung “after hangup”) zu wechseln. Dazu gab es bereits die Option
endbeforehexten=yes
in der Datei /etc/asterisk/cdr.conf. Allerdings führte das zum kompletten Crash der Software, also wurde dies wieder verworfen.
So gehts: CDR(duration) im hangup-Kontext selbst gemacht
Mit ein bisschen Bastelei lassen sich aber die benötigten Daten weitgehend selbst erstellen. Dazu muss man wissen, dass zwar CDR(billsec) und CDR(duration) stets auf 0 stehen, aber der derzeitige Zeitstempel immer per ${EPOCH} zur Verfügung steht. Ebenso steht die Abnehm-Zeit in CDR(answer) zur Verfügung. Ob CDR(start) da ist, kann ich gerade nicht genau beantworten, aber dies lässt sich ebenso ableiten. Man nutze einfach lokale Variablen:
[Mein-Kontext]
exten => s,1,Set(Start=${EPOCH})
exten => s,n,Dial(SIP/100,30,rtT)
exten => h,1,Set(Ende=${EPOCH})
exten => h,n,NoOp(Dauer: $[${Ende} - ${Start}])
exten => h,n,Hangup()
Wer die Syntax von Asterisk nicht kennt, wird jetzt wahrscheinlich die Hände über den Kopf zusammenschlagen, aber so ist Asterisk nun einmal: Friss oder Stirb
Also bevorgen die Herrschaften folgendermaßen zu speisen: Vor dem Aufbau der SIP-Verbindung per Dial() -Funktion wird der aktuelle Zeitstempel in die Variable Start gegossen. Und nach dem Auflegen wird als erstes der aktuelle Zeitstempel in die Variable Ende gelegt. Mithilfe der NoOp()-Funktion wird eine Ausgabe auf der Asterisk-Console durchgeführt, in der eine Berechnung $[...] stattfindet, worin Ende – Start, also die Dauer ab dem Klingeln ausgegeben wird: Fertig ist CDR(duration), verfügbar im hangup-Kontext.
So gehts: CDR(billsec) im hangup-Kontext selbst gemacht
Auf gleiche Weise lösen wir nun das Problem mit der fehlenden Gesprächsdauer. Dazu muss man wissen, dass der Zeitpunkt des Abhebens schon zur Laufzeit in CDR(answer) anliegt. Allerdings nicht als Unix-Timestamp (ein Ganzzahliger Sekundenwert seit dem 01.01.1970) wie oben in ${EPOCH}, sondern als Zeitangabe im Stil “Jahr-Monat-Tag Stunde:Minute:Sekunde”, also für Kenner von date()-Funktionen die Syntax “Y-m-d H:i:s”. Sehr schön zu lesen, leider innerhalb von Asterisk nicht so schön brauchbar. Allerdings lässt sich immerhin mit Hilfe der Asterisk-Funktion LEN() sagen, ob überhaupt abgenommen wurde oder nicht:
exten => h,1,GotoIf($[${LEN(0${CDR(answer)})} > 1]?Angenommen,Nichtangenommen)
exten => h,n(Angenommen),NoOp(Juhu, Angenommen!)
exten => h,n,Hangup()
exten => h,n(Nichtangenommen),NoOp(Noe, nicht angenommen!)
exten => h,n,Hangup()
Diese Syntax schon wieder! Hier muss man folgendes bemerken: Ein Vergleich in $[...] benutzt einen variablen Wert ${…}, der aus der Funktion CDR(answer) kommt, allerdings schreiben wir noch eine 0 davor. Dies hat folgende Bewandnis: Sollte nicht abgenommen worden sein, so ist CDR(answer) nicht gesetzt. Asterisk hat dann das Problem, eine ungesetzte Variable zu benutzen und meckert herum. Um dieses Hinderniss zu umschiffen, nehmen wir einfach vorn eine Null hinzu und schauen, ob die Variablenlänge größer als 1 ist.
Die Extension-Einträge 2 und 4 benutzen Labels, die per GotoIf() angesprungen werden können: Nach der Bedingung kommt ein Fragezeichen, dann das Label, falls alles geklappt hat, nach einem Komma dann der else-Zweig. Der else-Zweig ist hier nach dem If-Zweig notiert und würde eigentlich beim Zutreffen der Bedingung mit ausgeführt. Hier wirkt die Hangup()-Funktion also wie eine Art break-Anweisung innerhalb einer switch-Verzweigung.
Wie kommen wir jetzt an die Dauer? Nun, wir müssen den Wert CDR(answer) in einen Zeitstempel umrechnen. Aber das überlassen wir einfach einem externen Abrechnungstool. Denn intern brauchen wir es eigentlich nicht mehr:
exten => h,1,GotoIf($[${LEN(${0CDR(answer)})} > 1]?Angenommen,Nichtangenommen)
exten => h,n(Angenommen),NoOp(Juhu, Angenommen!)
exten => h,n,System('php -f /mein/pfad/cdr_abrechnung.php \
${CallerID(num)} ${EXTEN} "${CDR(answer)}" ${EPOCH}');
exten => h,n,Hangup()
exten => h,n(Nichtangenommen),NoOp(Noe, nicht angenommen!)
exten => h,n,Hangup()
Damit rufen wir das Skript zum Beispiel mit folgenden Variablen auf:
$argv = array ( [0] ... [1] => 0123456789 [2] => 100 [3] => 2009-02-06 20:07:00 [4] => 12392827161 );
mit strtotime() lässt sich $argv[3] direkt in einen Zeitstempel umrechnen. Von $argv[4] subtrahiert, ergibt sich CDR(billsec). Geschafft – Das Logging-Skript hat nun alle Daten, die es braucht!

Hi quark1980, wie habt ihr das mit ResetCDR(w) gelöst ??
Danke und Gruss
Geht das bereits bei Asterisk 1.2? Ich werde es morgen gleich mal probieren, danke auf jeden Fall für den Tip!
Wir hatten bei einem Kunden ein ähnliches Problem.
Warum führst du nicht ResetCDR(w) aus bevor du nach h-context switcht? Das ist doch um einiges eleganter. Aber OK, wir mussten dafür auch ne weile recherchieren