Ein AGI-Skript in Python für Asterisk erkennt die Caller-ID und trifft danach Entscheidungen

9. März 2023

Dieses AGI-Skript in Python gibt die Caller-ID und damit in der Regel die Telefonnummer des Anrufers zurück und verarbeitet sie weiter. Fängt die Nummer nicht mit Null an, wird die Caller-ID als Nummer angesagt. Fängt die Caller-ID mit einer Null an, wird die Nummer als Folge von einzelnen Ziffern aufgesagt. Damit erklärt diese Programm die Prinzipien, wie Caller-IDs eingelesen werden können und anschließend weiterverarbeitet werden.

Das nachfolgend vorgestellte AGI-Skript für Python kann als Grundlage dienen, um mit Hilfe der angerufenen Telefonnummer Entscheidungen für die Rufannahme oder die Behandlung in einem Auswahlmenü zu treffen.

Eine praktische Anwendung nach diesem Muster wäre es befreundete und bekannte Anrufer unter Umgehung des Auswahlmenüs direkt zur Zielnummer leiten zu lassen. Bestimmte Länder können an Hand der Ländervorwahlen geblockt werden oder sie landen auf Auswahlmenüs mit den entsprechenden Sprachen.

Hier das Skript, dass die Nummer des Anrufers aufsagt. Im Beispiel trägt es den Namen callerid.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import re
import time

# Read the incoming AGI environment variables
env = {}
tests = 0;

while 1:
   line = sys.stdin.readline().strip()

   if line == '':
      break
   key,data = line.split(':')
   if key[:4] != 'agi_':
      #skip input that doesn't begin with agi_
      sys.stderr.write("Did not work!\n");
      sys.stderr.flush()
      continue
   key = key.strip()
   data = data.strip()
   if key != '':
      env[key] = data
    
sys.stderr.write("AGI Environment Dump:\n");
sys.stderr.flush()
for key in env.keys():
   sys.stderr.write(" -- %s = %s\n" % (key, env[key]))
   sys.stderr.flush()
   
def checkresult (params):
   params = params.rstrip()
   if re.search('^200',params):
      result = re.search('result=(\d+)',params)
      if (not result):
         sys.stderr.write("FAIL ('%s')\n" % params)
         sys.stderr.flush()
         return -1
      else:
         result = result.group(1)
         #debug("Result:%s Params:%s" % (result, params))
         sys.stderr.write("PASS (%s)\n" % result)
         sys.stderr.flush()
         return int(result)
   else:
      error_code = re.search('result=(-?\d+)',params)
      if error_code:
         error_code = int(error_code.group(1))
         sys.stderr.write("FAIL (unexpected result '%s', error code %d)\n" % (params, error_code))
         sys.stderr.flush()
         return error_code
      else:
         sys.stderr.write("FAIL (unexpected result '%s')\n" % params)
         sys.stderr.flush()
         return -2

      
# https://metacpan.org/pod/Asterisk::AGI
      
def saynumber (params):
   sys.stderr.write("SAY NUMBER %s \"\"\n" % params)
   sys.stderr.flush()
   sys.stdout.write("SAY NUMBER %s \"\"\n" % params)
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   return checkresult(result)
   
def saydigits (params):
   sys.stderr.write("SAY DIGITS %s \"\"\n" % params)
   sys.stderr.flush()
   sys.stdout.write("SAY DIGITS %s \"\"\n" % params)
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)
   
def sayit (params):
   sys.stderr.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stderr.flush()
   sys.stdout.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)

# Get the Caller ID from the environment
callerid = env.get('agi_callerid', '')


time_duration = 1
time.sleep(time_duration) 

## Ansage #########################################

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/hello")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/your")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/telephone-number")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/is")
time_duration = 1
time.sleep(time_duration)

#############################################

# Send the Caller ID to Asterisk to be spoken
say_number_result = saynumber(callerid);

if say_number_result != -1:
    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)
    
    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)

    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)  
else:        
    print("Fehler: Caller-ID beginnt mit 0")
    sys.stdout.flush()
    print("und kann deshalb nicht aufgesagt werden.")
    sys.stdout.flush()
    
time_duration = 2
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


######################## DIGITS ##########################
# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()


sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


sayit("/usr/share/asterisk/sounds/en_US_f_Allison/goodbye")
time_duration = 2
time.sleep(time_duration)

# Wait for Asterisk to send a command to hang up the call
# sys.stdin.readline()

##############################################
# Aufruf in der extensions.conf              #
#                                            #
#exten => 344,1,Answer()                     #
#  same => n,Set(callerid=${CALLERID(num)})  #
#  same => n,AGI(callerid.py,${callerid})    #
#  same => n,Hangup()                        #
##############################################

Und so wird das Skript in der extensions.conf aufgerufen, wenn jemand die 244 wählt:

exten => 344,1,Noop(Anruf erfolgt von: "${CALLERID(name)}" <${CALLERID(num)}>) 
 same => n,Answer()
 same => n,Set(callerid=${CALLERID(num)})
 same => n,AGI(callerid.py,${callerid})
 same => n,Hangup()

Die erste Zeile “exten => 344,1,Noop(Anruf erfolgt von: “${CALLERID(name)}” <${CALLERID(num)}>)” ist sehr praktisch und lässt im Aserisk-CLI erkennen, wer anruft und wie die Caller-ID als Nummer gestaltet ist.  Weitere Erklärungen weiter unten im Text.

Welche Meldung erfolgt im Asterisk-CLI beim Anrufen der Nummer 344? Zuerst habe ich “agi set debug on” gesetzt, damit man mehr Meldungen über das Verhalten des AGI-Skripts erhält:

raspberrypi*CLI> agi set debug on
AGI Debugging Enabled
  == Using SIP RTP TOS bits 184
  == Using SIP RTP CoS mark 5
       > 0x73f65f18 -- Strict RTP learning after remote address set to: 192.168.1.44:10000
    -- Executing [344@janson:1] NoOp("SIP/1088-0000002e", "Anruf erfolgt von: "Volker Lange-Janson" <1088>") in new stack
    -- Executing [344@janson:2] Answer("SIP/1088-0000002e", "") in new stack
       > 0x73f65f18 -- Strict RTP switching to RTP target address 192.168.1.44:10000 as source
    -- Executing [344@janson:3] Set("SIP/1088-0000002e", "callerid=1088") in new stack
    -- Executing [344@janson:4] AGI("SIP/1088-0000002e", "callerid.py,1088") in new stack
    -- Launched AGI Script /usr/share/asterisk/agi-bin/callerid.py
<SIP/1088-0000002e>AGI Tx >> agi_request: callerid.py
<SIP/1088-0000002e>AGI Tx >> agi_channel: SIP/1088-0000002e
<SIP/1088-0000002e>AGI Tx >> agi_language: en
<SIP/1088-0000002e>AGI Tx >> agi_type: SIP
<SIP/1088-0000002e>AGI Tx >> agi_uniqueid: 1678357658.101
<SIP/1088-0000002e>AGI Tx >> agi_version: 16.2.1~dfsg-1+deb10u2
<SIP/1088-0000002e>AGI Tx >> agi_callerid: 1088
<SIP/1088-0000002e>AGI Tx >> agi_calleridname: Volker Lange-Janson
<SIP/1088-0000002e>AGI Tx >> agi_callingpres: 0
<SIP/1088-0000002e>AGI Tx >> agi_callingani2: 0
<SIP/1088-0000002e>AGI Tx >> agi_callington: 0
<SIP/1088-0000002e>AGI Tx >> agi_callingtns: 0
<SIP/1088-0000002e>AGI Tx >> agi_dnid: 344
<SIP/1088-0000002e>AGI Tx >> agi_rdnis: unknown
<SIP/1088-0000002e>AGI Tx >> agi_context: janson
<SIP/1088-0000002e>AGI Tx >> agi_extension: 344
<SIP/1088-0000002e>AGI Tx >> agi_priority: 4
<SIP/1088-0000002e>AGI Tx >> agi_enhanced: 0.0
<SIP/1088-0000002e>AGI Tx >> agi_accountcode:
<SIP/1088-0000002e>AGI Tx >> agi_threadid: 1851048864
<SIP/1088-0000002e>AGI Tx >> agi_arg_1: 1088
<SIP/1088-0000002e>AGI Tx >>
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/hello ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/hello.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=6400
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/your ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/your.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=5120
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/telephone-number ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/telephone-number.gsm' (escape_digits=) (sample_offset 0) (language 'en')
       > 0x73f65f18 -- Strict RTP learning complete - Locking on source address 192.168.1.44:10000
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=11200
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/is ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/is.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=4960
<SIP/1088-0000002e>AGI Rx << SAY NUMBER 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/thousand.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/80.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << SAY NUMBER 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/thousand.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/80.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << SAY NUMBER 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/thousand.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/80.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << SAY NUMBER 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/thousand.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/80.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << SAY DIGITS 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/0.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << SAY DIGITS 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/0.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << SAY DIGITS 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/0.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << SAY DIGITS 1088 ""
    -- <SIP/1088-0000002e> Playing 'digits/1.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/0.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
    -- <SIP/1088-0000002e> Playing 'digits/8.gsm' (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/beep ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/beep.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=3520
<SIP/1088-0000002e>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/en_US_f_Allison/goodbye ""
    -- <SIP/1088-0000002e> Playing '/usr/share/asterisk/sounds/en_US_f_Allison/goodbye.gsm' (escape_digits=) (sample_offset 0) (language 'en')
<SIP/1088-0000002e>AGI Tx >> 200 result=0 endpos=7520
    -- <SIP/1088-0000002e>AGI Script callerid.py completed, returning 0
    -- Executing [344@janson:5] Hangup("SIP/1088-0000002e", "") in new stack
  == Spawn extension (janson, 344, 5) exited non-zero on 'SIP/1088-0000002e'
raspberrypi*CLI>

Hier sind die einzelnen Abschnitte dieses AGI-Skripts der Reihe nach vorgestellt, da dieses Skript als Muster für weiter Skripte dienen kann. Es läuft auf Python 2.7  und Python 3:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import re
import time

Dies ist ein Python-Skript, das zur Integration von Asterisk verwendet wird. Es beginnt mit der Angabe der Umgebung, in der es ausgeführt wird, und der Kodierung des Skripts.

Als nächstes werden drei Bibliotheken importiert: sys, re und time. sys ist eine Standardbibliothek, die Funktionen und Variablen zur Interaktion mit dem Python-Interpreter bereitstellt. re ist eine Bibliothek, die reguläre Ausdrücke unterstützt, und time ermöglicht das Arbeiten mit der Zeit.

Das Skript wird in einem Asterisk-Kontext ausgeführt und arbeitet mit Informationen, die von Asterisk bereitgestellt werden. Der genaue Zweck des Skripts hängt von den folgenden Abschnitten ab.

# Read the incoming AGI environment variables
env = {}
tests = 0;

while 1:
   line = sys.stdin.readline().strip()

   if line == '':
      break
   key,data = line.split(':')
   if key[:4] != 'agi_':
      #skip input that doesn't begin with agi_
      sys.stderr.write("Did not work!\n");
      sys.stderr.flush()
      continue
   key = key.strip()
   data = data.strip()
   if key != '':
      env[key] = data
    
sys.stderr.write("AGI Environment Dump:\n");
sys.stderr.flush()
for key in env.keys():
   sys.stderr.write(" -- %s = %s\n" % (key, env[key]))
   sys.stderr.flush()

Dieser Abschnitt liest die Eingabevariablen der AGI (Asterisk Gateway Interface) Umgebung und speichert sie in einem Wörterbuch namens env. Dies geschieht durch die Verwendung von sys.stdin.readline(), um die Eingabezeilen von Asterisk zu lesen, die von einem Doppelpunkt getrennte Schlüssel-Wert-Paare enthalten.

Zunächst wird eine leere Umgebung env und eine Zählvariable tests initialisiert. Dann wird eine Schleife begonnen, die auf unbestimmte Zeit ausgeführt wird (while 1). In jeder Iteration der Schleife wird eine Zeile von der Standardeingabe sys.stdin gelesen, mit der strip()-Methode der Zeilenumbruch entfernt und überprüft, ob es sich um eine leere Zeile handelt. Wenn ja, wird die Schleife beendet.

Dann wird die Zeile in Schlüssel und Daten aufgeteilt, wobei der Doppelpunkt als Trennzeichen dient. Wenn der Schlüssel nicht mit “agi_” beginnt, wird die Eingabe ignoriert, indem eine Fehlermeldung ausgegeben und die Schleife fortgesetzt wird. Andernfalls werden der Schlüssel und die Daten in env gespeichert.

Schließlich werden die Umgebungsinformationen in der Fehlerausgabe mit der Nachricht “AGI Environment Dump” ausgegeben. Eine Schleife durchläuft alle Schlüssel-Wert-Paare des env Wörterbuchs und gibt jeden Eintrag in der Form “Schlüssel = Wert” aus. Die Fehlerausgabe wird nach jedem Eintrag geflusht, um sicherzustellen, dass die Ausgabe unmittelbar angezeigt wird.

def checkresult (params):
   params = params.rstrip()
   if re.search('^200',params):
      result = re.search('result=(\d+)',params)
      if (not result):
         sys.stderr.write("FAIL ('%s')\n" % params)
         sys.stderr.flush()
         return -1
      else:
         result = result.group(1)
         #debug("Result:%s Params:%s" % (result, params))
         sys.stderr.write("PASS (%s)\n" % result)
         sys.stderr.flush()
         return int(result)
   else:
      error_code = re.search('result=(-?\d+)',params)
      if error_code:
         error_code = int(error_code.group(1))
         sys.stderr.write("FAIL (unexpected result '%s', error code %d)\n" % (params, error_code))
         sys.stderr.flush()
         return error_code
      else:
         sys.stderr.write("FAIL (unexpected result '%s')\n" % params)
         sys.stderr.flush()
         return -2

Dieser Abschnitt definiert die Funktion checkresult(), die verwendet wird, um die Ergebnisse der AGI-Anweisungen zu überprüfen. Die Funktion erhält einen String-Parameter params, der das Ergebnis der letzten ausgeführten AGI-Anweisung enthält.

Die Funktion rstrip() wird aufgerufen, um alle führenden und abschließenden Leerzeichen von params zu entfernen. Dann wird überprüft, ob das Ergebnis mit “200” beginnt, was darauf hinweist, dass die Anweisung erfolgreich ausgeführt wurde. Wenn ja, wird das Ergebnis auf den Wert des regulären Ausdrucks “result=(\d+)” abgeglichen, um das Ergebnis der Anweisung zu erhalten. Wenn es keinen Übereinstimmung gibt, wird eine Fehlermeldung ausgegeben und -1 zurückgegeben. Andernfalls wird das Ergebnis in einen Integer umgewandelt und zurückgegeben.

Wenn das Ergebnis nicht mit “200” beginnt, wird das Ergebnis auf den Wert des regulären Ausdrucks “result=(-?\d+)” abgeglichen, um einen Fehlercode zu erhalten. Wenn ein Fehlercode gefunden wird, wird dieser in einen Integer umgewandelt und eine Fehlermeldung ausgegeben, die den Fehlercode und den unerwarteten Ergebniswert enthält. Andernfalls wird eine Fehlermeldung ausgegeben, die den unerwarteten Ergebniswert enthält, und -2 zurückgegeben.

def saynumber (params):
   sys.stderr.write("SAY NUMBER %s \"\"\n" % params)
   sys.stderr.flush()
   sys.stdout.write("SAY NUMBER %s \"\"\n" % params)
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   return checkresult(result)

Dieser Abschnitt definiert die Funktion saynumber(), die verwendet wird, um eine nummerische Eingabeaufforderung an den Anrufer zu senden. Die Funktion erhält einen String-Parameter params, der die zu sprechende Zahl enthält.

Zuerst wird eine Fehlermeldung mit dem Text “SAY NUMBER” und dem Wert von params an sys.stderr geschrieben und dann sys.stderr.flush() aufgerufen, um sicherzustellen, dass die Fehlermeldung sofort ausgegeben wird.

Dann wird ein String erstellt, der den AGI-Befehl “SAY NUMBER” und den Wert von params enthält. Dieser String wird an sys.stdout geschrieben und dann sys.stdout.flush() aufgerufen, um sicherzustellen, dass der String sofort an Asterisk übermittelt wird.

Anschließend wird auf eine Antwort von Asterisk gewartet, indem sys.stdin.readline() aufgerufen wird. Der zurückgegebene String wird mit strip() bereinigt und dann wird die checkresult()-Funktion aufgerufen, um das Ergebnis zu überprüfen und zurückzugeben.

def saydigits (params):
   sys.stderr.write("SAY DIGITS %s \"\"\n" % params)
   sys.stderr.flush()
   sys.stdout.write("SAY DIGITS %s \"\"\n" % params)
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)

Dieser Abschnitt definiert die Funktion saydigits(), die verwendet wird, um eine Aufforderung an den Anrufer zu senden, um Ziffern zu sprechen. Die Funktion erhält einen String-Parameter params, der die zu sprechenden Ziffern enthält.

Wie bei der saynumber()-Funktion wird zunächst eine Fehlermeldung mit dem Text “SAY DIGITS” und dem Wert von params an sys.stderr geschrieben und dann sys.stderr.flush() aufgerufen, um sicherzustellen, dass die Fehlermeldung sofort ausgegeben wird.

Dann wird ein String erstellt, der den AGI-Befehl “SAY DIGITS” und den Wert von params enthält. Dieser String wird an sys.stdout geschrieben und dann sys.stdout.flush() aufgerufen, um sicherzustellen, dass der String sofort an Asterisk übermittelt wird.

Anschließend wird auf eine Antwort von Asterisk gewartet, indem sys.stdin.readline() aufgerufen wird. Der zurückgegebene String wird mit strip() bereinigt und dann wird die checkresult()-Funktion aufgerufen, um das Ergebnis zu überprüfen. Das Ergebnis wird in diesem Fall jedoch nicht zurückgegeben.

def sayit (params):
   sys.stderr.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stderr.flush()
   sys.stdout.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)

Dieser Abschnitt definiert die Funktion sayit(), die verwendet wird, um eine Audiodatei abzuspielen, die auf dem Asterisk-Server gespeichert ist. Die Funktion erhält einen String-Parameter params, der den Namen der abzuspielenden Datei enthält.

Wie bei den anderen Funktionen wird zunächst eine Fehlermeldung mit dem Text “STREAM FILE” und dem Wert von params an sys.stderr geschrieben und dann sys.stderr.flush() aufgerufen, um sicherzustellen, dass die Fehlermeldung sofort ausgegeben wird.

Dann wird ein String erstellt, der den AGI-Befehl “STREAM FILE” und den Wert von params enthält. Dieser String wird an sys.stdout geschrieben und dann sys.stdout.flush() aufgerufen, um sicherzustellen, dass der String sofort an Asterisk übermittelt wird.

Anschließend wird auf eine Antwort von Asterisk gewartet, indem sys.stdin.readline() aufgerufen wird. Der zurückgegebene String wird mit strip() bereinigt und dann wird die checkresult()-Funktion aufgerufen, um das Ergebnis zu überprüfen. Das Ergebnis wird in diesem Fall jedoch nicht zurückgegeben.

Nachfolgend fängt das eigentliche Programm an:

# Get the Caller ID from the environment
callerid = env.get('agi_callerid', '')

In diesem Abschnitt wird der Anrufer-ID-String aus der Umgebungsvariablen agi_callerid ausgelesen und der Variablen callerid zugewiesen. Der Wert von agi_callerid enthält normalerweise die Telefonnummer des Anrufers, die vom Telefonanbieter bereitgestellt wird. Wenn jedoch keine Anrufer-ID verfügbar ist oder der Anrufer seine Nummer blockiert hat, ist der Wert leer oder ein Leerzeichen.

time_duration = 1
time.sleep(time_duration) 

## Ansage #########################################

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/hello")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/your")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/telephone-number")
time_duration = 1
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/is")
time_duration = 1
time.sleep(time_duration)

In diesem Abschnitt werden Pausen zwischen den einzelnen Ansagen eingelegt und dann vier verschiedene Ansagen abgespielt. Die Dauer jeder Pause und die zu spielenden Sound-Dateien sind festgelegt. Es wird der Text “Hello”, “Your”, “Telephone Number” und “Is” von einer Stimme namens Allison auf Englisch gesprochen. Die Pausen dienen dazu, die Ansagen deutlicher zu trennen und für eine bessere Verständlichkeit zu sorgen.

# Send the Caller ID to Asterisk to be spoken
say_number_result = saynumber(callerid);

if say_number_result != -1:
    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)
    
    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)

    saynumber(callerid);
    sys.stdout.flush()
    time_duration = 2
    time.sleep(time_duration)  
else:        
    print("Fehler: Caller-ID beginnt mit 0")
    sys.stdout.flush()
    print("und kann deshalb nicht aufgesagt werden.")
    sys.stdout.flush()
    
time_duration = 2
time.sleep(time_duration)

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)

In diesem Abschnitt wird die Caller ID, also die Rufnummer des Anrufers, an Asterisk gesendet, um sie von einer Stimme ausgeben zu lassen. Zunächst wird die Funktion saynumber() aufgerufen und das Ergebnis in der Variablen say_number_result gespeichert. Wenn say_number_result ungleich -1 ist, werden die saynumber()-Funktion drei Mal aufgerufen, um die Caller ID dreimal hintereinander von der Stimme ausgeben zu lassen. Zwischen den Aufrufen wird eine Pause von jeweils zwei Sekunden eingelegt. Falls die Caller ID mit einer 0 beginnt und deshalb nicht aufgesagt werden kann, wird eine Fehlermeldung ausgegeben und das Skript springt zur Ausgabe des Signaltons “beep”.

######################## DIGITS ##########################
# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()


sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


# Send the Caller ID to Asterisk to be spoken in digits
saydigits(callerid);
sys.stdout.flush()

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
time_duration = 2
time.sleep(time_duration)


sayit("/usr/share/asterisk/sounds/en_US_f_Allison/goodbye")
time_duration = 2
time.sleep(time_duration)

Zu Beginn wird im obigen Abschnitt die Anrufer-ID aus der Umgebung gelesen und in der Variablen callerid gespeichert. Anschließend wird eine kurze Pause eingelegt und die Funktion sayit genutzt, um eine Begrüßung auszusprechen. Darauf folgen weitere Pausen und sayit-Aufrufe, um den Text “your telephone number is” auszusprechen.

Danach wird die Funktion saynumber genutzt, um die Anrufer-ID dreimal nacheinander auszusprechen. Falls die Anrufer-ID mit “0” beginnt, wird eine Fehlermeldung ausgegeben.

Anschließend werden die Funktion saydigits und sayit genutzt, um die Anrufer-ID zweimal in Ziffern auszusprechen und jeweils einen Piepton zu erzeugen.

Dieser Vorgang wird insgesamt viermal wiederholt, gefolgt von einer Abschiedsnachricht.

Und so wird das AGI-Skript in der extensions.conf aufgerufen:

##############################################
# Aufruf in der extensions.conf              
#                                            
exten => 344,1,Answer()                     
 same => n,Set(callerid=${CALLERID(num)})  
 same => n,AGI(callerid.py,${callerid})    
 same => n,Hangup()                        
##############################################

Diese Konfiguration in der extensions.conf ist eine Beispielkonfiguration für eine Asterisk-Erweiterung (Extension), die Anrufe von der Rufnummer 344 annimmt.

Der erste Schritt in der Erweiterung ist das Abnehmen des Anrufs mit dem Answer()-Befehl. Danach wird die Rufnummer des Anrufers im callerid-Variable gespeichert, und diese Variable wird an das Python-Skript callerid.py mit dem AGI()-Befehl übergeben, um die Caller-ID zu verarbeiten.

Die  Zeile, “same => n,Set(callerid=${CALLERID(num)})” setzt die Variable “callerid” auf die aktuelle Rufnummer des Anrufers, die durch die Variable “CALLERID(num)” bereitgestellt wird.

Die Zeile, “same => n,AGI(callerid.py,${callerid})“, ruft das AGI-Skript “callerid.py” auf und übergibt die Rufnummer als Parameter. Das Skript wird dann ausgeführt, um die Rufnummer des Anrufers auszusprechen. Anschließend wird der Anruf beendet, indem die “Hangup()” Anweisung ausgeführt wird.

Der Programmier-Code ist fehlerfrei. Trotzdem läuft das AGI-Skript nicht. Warum verflixt noch einmal?
AGI-Skripte für Asterisk unter Windows editieren – Was ist zu beachten? – 9. März 2023: Windows ist weit verbreitet. Deshalb schreiben nicht wenige ihre AGI-Skripte auf Windows, um sie dann in ihren Raspberry Pi zu kopieren. Wer einige Dinge nicht beachtet, wird die Skripte nicht zum Laufen bekommen. Hier sind die Fallstricke beschrieben, die mich viel Zeit gekostet haben. Die Beispiele beziehen sich auf Python. Sie haben für andere Programmiersprachen die entsprechende Gültigkeit. – weiter

Übergeordnet:

Auf einem Raspberry Pi lässt sich ein kompletter Telefonserver betreiben, der seinen Teilnehmern über Landesgrenzen hinweg kostenloses Telefonieren von unterwegs oder daheim ermöglicht.
Asterisk-Telefonserver auf einem Raspberry Pi – Installation, Konfiguration, Programmierung, SIP, IAX2, AGI-Skripte, Sicherheit und Tipps zum praktischen Betrieb – 2.11.2022: Diese Seite richtet sich an jene, welche einen Asterisk-Telefon-Server auf einem Raspberry Pi betreiben möchten und später ein kleines Netzwerk aus Asterisk-Servern planen, um ein eigenständiges Telefonnetz aufzubauen. Los geht es mit der Installation von Raspbian und Asterisk auf einem Raspberry Pi und dann nach Lust und Laune immer tiefer in die Programmierung von Asterisk. Die Themen werden laufend erweitert.

Selbstverständlich muss es nicht unbedingt ein Raspberry Pi sein. Andere Linux-Rechner gehen auch. – weiter