Auswahlmenü kombiniert mit CallThrough als AGI-Skript in Python für Asterisk

18. Februar 2023

Mit diesem AGI-Skript in Python für Asterisk gelangt der Anrufende in ein Auswahlmenü. Nach dem Eingeben einer zweistelligen Nummer erhält er Zugang zu externen Telefonnetzen oder zu anderen Telefon-Servern. Nach dieser Vorauswahl kann er eine Zielnummer eintippen, um ein Endgerät im  ausgewählten externen Telefonnetz oder Server zu erreichen.

Bedienung und Ablauf: Nach der Einwahl über eine bestimmte Einwahlnummer (im Beispiel zu Testzwecken 501) wird das Python-Skript aufgerufen. Es erfolgt eine Ansage, die erklärt welche der 12 Netze über welche zweistelligen Nummern erreichbar sind. Sobald der Nutzer diese Auswahl getroffen hat, wird die Ansage unterbrochen. Es erfolgt eine weitere Aufforderung die gewünschte Zielnummer im gewählten Netz einzutippen. Wenn 5 Sekunden lang keine weitere Eingabe über die Tastatur erfolgt, ertönt die Ansage, dass man nun weiterverbunden wird. Dann hört der Teilnehmer den Signalton und er wird verbunden. Gibt der Teilnehmer nach 5 Sekunden keine Ziffer ein, erfolgt eine Fehlermeldung und das Programm sorgt für das Auflegen des Telefons. Versäumt der Nutzer innerhalb von 5 Sekunden seine zweistellige Auswahlnummer einzugeben oder wählt eine ungültige Nummer, wird er in einer  Endlosschleife erneut aufgefordert die Auswahl zu treffen.

Das Programm besteht im Kern aus einer if-Anweisung, die auf  Grund der zweistelligen Nummern die Auswahl trifft. Die Auswahl erfolgt über die Wahl des Kontextes, denn für jedes externe Netz existiert ein isolierter Kontext in der extensions.conf. Diese Kontexte sind also nur über das Python-Programm erreichbar, um Konflikte mit anderen  Wahlregeln zu vermeiden.

Wurde mit dieser if-Anweisung der richtige Kontext getroffen, muss nun die entsprechende Nummer gewählt werden,  was wiederum mit einer weiteren if-Anweisung geschieht. In diesem Zusammenhang erfolgt die Aufforderung eine Nummer einzutippen. Ist die Nummer kleiner als Null, was programmintern passiert, wenn keine Nummer eingegeben wurde, erfolgt eine Fehlermeldung. Andernfalls wird die Nummer gewählt und es eine Stimme bestätigt den  Verbindungsaufbau. Die zweite if-Anweisung befindet sich in einer Funktion, um das Skript kompakter zu halten.

Asterisk-Konsole: Es passiert eine Menge, wenn das hier vorgestellte AGI-Skript in Aktion ist.

Zudem sind noch weitere Funktionen am Anfang des Skriptes enthalten, die für die Kommunikation mit Asterisk benötigt werden. Sie sind auf

http://www.asteriskdocs.org/en/2nd_Edition … asterisk-CHP-9-SECT-4.html

beschrieben. Dadurch kommt das Programm ohne spezielle Asterisk-Module aus. Es benötigt also kein Pyst, Pyst2 oder Pyst3. Getestet ist das Programm auf Python 2.7. Es müsste aber auch auf Python 3 laufen. Einige Programmschnipsel sind mit Hilfe von ChatGPT erstellt worden, die dann nachgebessert werden mussten.

Das Python-Skript externeprovider.py: In welchem Ordner es unterzubringen ist, steht in der asterisk.conf. Diese befindet sich in dem Ordner, in dem sich auch die extensions.conf befindet.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Name der Datei: externeprovider.py
# 18:39 Freitag, 17. Februar 2023
# Auswahlmenue fuer externe Provider

# Linux-Befehle zum Debugging und zum auf UNIX umwandeln: Erspart langes Suchen nach den Befehlen
# sudo dos2unix /usr/share/asterisk/agi-bin/externeprovider.py  # auf UNIX umstellen für Linux
# sudo chmod 777 /usr/share/asterisk/agi-bin/externeprovider.py # wird ausführbar
# sudo python /usr/share/asterisk/agi-bin/externeprovider.py    # fürs Debugging

######### Nachfolgendes erspart den Einsatz von pyst #######################

# siehe Beschreibung auf
# http://www.asteriskdocs.org/en/2nd_Edition/asterisk-book-html-chunk/asterisk-CHP-9-SECT-4.html

import sys
import re
import time

# Read and ignore AGI environment (read until blank line)

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 result
   else:
      sys.stderr.write("FAIL (unexpected result '%s')\n" % params)
      sys.stderr.flush()
      return -2

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)

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()
   checkresult(result)

def getnumber (prompt, timelimit, digcount):
   sys.stderr.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stderr.flush()
   sys.stdout.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   result = checkresult(result)
   sys.stderr.write("digits are %s\n" % result)
   sys.stderr.flush()
   if result:
      return result
   else:
      result = -1

################### Ab hier beginnt das eigentliche Skript #####################

# Eingabe der Nummer eines externen Providers ohne Vorwahl
def number1(digitcountsnumber, ttanswersnumber):
    result1 = getnumber("/usr/share/asterisk/sounds/eigene/externprovider/waehlensie",ttanswersnumber,digitcountsnumber)
    if int(result1) < 0 :
        print("SET VARIABLE EXTNEW " + str(result1))
        sys.stdout.flush()
        sayit("/usr/share/asterisk/sounds/eigene/externprovider/fehler")
        time_duration = 5
        time.sleep(time_duration)
    else:
        print("SET VARIABLE EXTNEW " + str(result1))
        sys.stdout.flush() # ohne flush hoert man das nachfolgende soundfile nicht
        sayit("/usr/share/asterisk/sounds/eigene/externprovider/verbindungsaufbau")
        time_duration = 2
        time.sleep(time_duration)

limit=5         # nicht definiert
digitcount=2    # Der Nutzer kann nur max. 2 Ziffern eingeben und danach beginnt die Vermittlung
ttanswer=5000   # Wenn nach 5000 ms nichts mehr eingeben wird, ist die Zeit überschritten

digitcountsnumber = 50  # Maximal 50 Ziffern sind erlaubt bei Eingabe der Nummer
ttanswersnumber = 5000  # Wenn 5000 ms nichts mehr eingeben wird, ist die Zeit überschritten


time_duration = 1           # Ein Sekunde Pause eingebaut, damit der Nutzer den Anfang
time.sleep(time_duration)   # der Ansage mitbekommt.

# Nachfolgend erfolgt das Abspielen eines WAV- oder GSM-Soundfiles, dass den Nutzer
# informiert welche Ziffer er zu wählen hat. Der Nutzer kann während des Abspielens
# der Sounddatei die gewünschte Ziffer wählen. Sogleich wird er weiterverbunden.
# Die Sounddateien lassen sich z.B. mit Text to Speech auf https://ttsmp3.com/ als MP3 erstellen
# und anschließend auf https://g711.org/ mit der Standardeinstellung in das g.711-Format
# als Wav-Datei umwandeln, die für Asterisk  geeignet ist.

number = getnumber("/usr/share/asterisk/sounds/eigene/externprovider/halloliste",ttanswer,digitcount)

# number ist die Ziffer oder die Zahl, die vom Nutzer in die Tastatur des Telefons eingetippt
# wurde.

# In der if-elif-else Anweisung wird sie in eine Ganzzahl umgewandelt. Diese If-Aweisung
# bestimmt, welche Nummern (Extensions) gewählt werden, wenn man
# eine Ziffer oder Zahl eintippt.

# Tippt man die z.B. die 01 ein, dann wird mit Print ein String ausgedruckt,
# der die Variable CONTEXTNEW setzt. Das ist der Context für die Extension 
# des externen Providers

if int(number) == 1:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_cnet") 
elif int(number) == 2:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_callcentric") 
elif int(number) == 3:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_iptel") 
elif int(number) == 4:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_sip2sip") 
elif int(number) == 5:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_antisip") 
elif int(number) == 6:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dusnet")   
elif int(number) == 7:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_opensips")    
elif int(number) == 8:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_voipfone")  
elif int(number) == 9:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_sipcall")
elif int(number) == 10:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_ippi") 
elif int(number) == 11:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dl1hrc")  
elif int(number) == 12:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dl5pd")      
else: 
    sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
    time_duration = 2
    time.sleep(time_duration) 

# Wenn der Nutzer die 5 Sekunden verstreichen lässt oder eine
# nicht definierte Zahl eintippt, springt das Skript zu
# else, es wird eine Sounddatei abgespielt die Piep macht
# und dann geht es wieder von vorne los.

#
# exampel in extensions.conf
#
# [telefone]
# exten => 501,1,Answer() # Hier im Demo die 501 wählen, um ins Menue zu kommen
#  same => n,AGI(externeprovider.py) # eventuell absolute Pfadangabe notwendig
#  same => n,NoOP(from script externeprovider.py dialing ${CONTEXTNEW} , ${EXTNEW})
#  same => n,Goto(${CONTEXTNEW},${EXTNEW},1) # Hier geht es zum Context und zur Extension
#  same => n,Hangup()


# ; Fuer jeden externen Provider gibt es einen eigenen isolierten Context,
# , um Konflikte mit Wahlregeln zu vermeiden. Drei Beispiele:
#
# [goto_iptel] 
# exten => _X.,1,Dial(SIP/${EXTEN}@iptel,60,tT)

# [goto_sip2sip] 
# exten => _X.,1,Dial(SIP/${EXTEN}@sip2sip,60,tT)

# [goto_cnet]
# exten => _X.,1,Dial(IAX2/outgoing-cnet/${EXTEN})

Aufruf des Skripts in der extensions.conf: Der Aufruf erfolgt im Beispiel durch die Wahl der Nummer 501. Das Skript wird ausgeführt, es werden zwei Parameter, jeweils einer für den Kontext (CONTEXTNEW) und einer für die gewählte Zielnummer (EXTNEW).

[telefone]

exten => 501,1,Answer() # Hier im Demo die 501 wählen, um ins Menue zu kommen
  same => n,AGI(externeprovider.py) # eventuell absolute Pfadangabe notwendig
  same => n,NoOP(from script externeprovider.py dialing ${EXTNEW}) and ${CONTEXTNEW}
  same => n,Goto(${CONTEXTNEW},${EXTNEW},1) # Hier geht es zum Context und zur Extension
  same => n,Hangup()

Für jedes externe Netz stehen die Wahlregeln in einem isolierten Kontext, die ebenfalls sich in der extensions.conf befinden.

[goto_cnet] ; C*Net 01
exten => _X.,1,Dial(IAX2/outgoing-cnet/${EXTEN})
same => n,Gosub(hinweise,s,1) ; Gosub nicht notwendig, erzeugt nur Hinweise, wenn Besetzt, falsche Nummer u.s.w.

[goto_callcentric] ; Callcentric 02
exten => _X.,1,Dial(SIP/${EXTEN}@callcentric,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_iptel] ; Iptel 03
exten => _X.,1,Dial(SIP/${EXTEN}@iptel,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_sip2sip] ; Sip2Sip 04
exten => _X.,1,Dial(SIP/${EXTEN}@sip2sip,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_antisip] ; AntiSip 05
exten => _X.,1,Dial(SIP/${EXTEN}@antisip,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_dusnet] ; Dusnet 06
exten => _X.,1,Dial(SIP/${EXTEN}@dusnet,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_opensips] ; Opensips 07
exten => _X.,1,Dial(SIP/${EXTEN}@opensips,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_voipfone] ; Voipfone 08
exten => _X.,1,Dial(SIP/${EXTEN}@voipfone,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_sipcall] ; SipCall Österreich 09
exten => _X.,1,Dial(SIP/${EXTEN}@sipcall,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_ippi] ; Ippi 10
exten => _X.,1,Dial(SIP/${EXTEN}@sipcall,ippi,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_dl1hrc] ; DL1HRC 11
exten => _X.,1,Dial(IAX2/outgoing-dl1hrc/${EXTEN})
same => n,Gosub(hinweise,s,1)

[goto_dl5pd] ; DL5PD 12
exten => _X.,1,Dial(IAX2/outgoing-rainer/${EXTEN})
same => n,Gosub(hinweise,s,1);

Dieser Aufwand war notwendig, damit die Wahlregeln nicht mit anderen Wahlregeln in Konflikt geraten können, denn die fremden Netze sind als Alternative noch über vierstellige Vorwahlen erreichbar, die auch mit Wählscheibentelefonen die Auswahl ermöglichen. Die Zeilen mit Gosub können entfallen. Sie liefern nur Hinweise, falls der Verbindungsaufbau scheitert.

Um als Beispiel die Wahlregeln für die Wahl in externe Netze zu verstehen, betrachten wir dies für die Einwahl in Iptel. Tippt jemand 03, sorgt das AGI-Skript dafür das die Variable CONTEXTNEW den String goto_iptel zurückgibt. Tippt jemand nach dieser Auswahl eine Nummer ein, dann wird diese an EXTNEW übergeben und dann wiederum an EXTEN.

[goto_iptel] ; Iptel 03 
exten => _X.,1,Dial(SIP/${EXTEN}@iptel,60,tT)

Diese Wahlregel muss also “@iptel” finden. Diese Anweisung findet  ihre Entsprechung in der sip.conf:

[general] ; sip.conf auszugsweise

;
;
; Die Registrierung ermöglicht Anrufe an Iptel entgegen zu nehmen
register => 1234567:meinpasswort@iptel.org/1234567


[iptel] ; Hier steht auch iptel
context=iptel-in
host=iptel.org
username=1234567
secret=meinpasswort
fromuser=1234567
fromdomain=iptel.org
canreinvite=no
qualify=yes
nat=force_rport,comedia
type=peer
port=5060

Nachfolgende Erklärungen sind nicht für das Verständnis des Skripts notwendig: Das Skript behandelt nur das Herauswählen in die externen Netze. Nachfolgend geht es um das Hineinwählen in den  Asterisk-Server von fremden Netzen, um das gesamte Prinzip bei dieser Gelegenheit zu verstehen.

Zur Vollständigkeit deshalb hier noch die Erklärung, was passiert, wenn jemand meinen Asterisk-Server über Iptel anruft durch die Iptel-Nummer 1234567:

[iptel-in]
; im Iptel-Netz die Nummer 1234567 wählen
exten => 1234567,1,Goto(callthrough,s,1) es geht zum Context callthrough mit der Einwahlroutine

Das [iptel-in] finden wir als Entsprechung in der sip.conf in [iptel]: Wir finden es unter [iptel] in der Zeile context=iptel-in.

; Auszug aus der Sip.conf

[iptel] ; Hier steht auch iptel 
context=iptel-in  ; hier steht iptel-in
host=iptel.org 
username=1234567 
secret=meinpasswort 
fromuser=1234567 
fromdomain=iptel.org 
canreinvite=no 
qualify=yes 
nat=force_rport,comedia 
type=peer 
port=5060

Und jetzt noch die alternative Wahlregel für das Hineinwählen in das Iptel-Netz über eine Vorwahl:

; wir sind in der extensions.conf

[telefone] ; hier stehen natürlich viele Wahlregeln und nicht nur eine

; Weiterleitung auf Iptel-Nummern (interne Vorwahl 8844)
exten => _8844X.,1,Dial(SIP/${EXTEN:4}@iptel,60,tT) ;

Wählt man als Vorwahl die 8844 und dann als Durchwahl sofort die gewünschte Nummer im Iptel-Netz gelangt man zu dieser Nummer. Das Patternmatching  “_8844X.” beinhaltet alle gewählten Nummern, die mit 8844 beginnen. Die Anweisung “${EXTEN:4}” sorgt dafür, dass von der gewählten Nummer die ersten vier Ziffern abzuschneiden sind. Wählt jemand zum Beispiel 884478901, wird die Iptel-Nummer 78901 angewählt.

Und nun das Ganze noch an Hand einer IAX2-Verbindung erklärt: Mein Server ist zum Beispiel mit dem von Rainer, DL5PD über IAX2 erreichbar, wenn im AGI-Skript die 12 gewählt wird. Dann gelangt man in den folgenden Kontext:

[goto_dl5pd] ; DL5PD 12
exten => _X.,1,Dial(IAX2/outgoing-rainer/${EXTEN}) ; outgoing-rainer ist entscheidend
same => n,Gosub(hinweise,s,1)

Im Prinzip fast wie bei SIP. Nachfolgend die Entsprechung in der iax.conf:

; wir sind in der iax.conf

[outgoing-rainer]       ; hier steht auch wieder das "outgoing-rainer"
type=peer               ; peer bedeutet nur ausgehend
username=incoming-volker
host=blabal.blall.de
secret=blablabla
context=telefone
trunk=yes
qualify=yes
requirecalltoken=no
calltokenoptional=yes
authdebug=yes

Und so sieht das alternative Anwählen von Rainers Server mit Vorwahl aus:

; wir sind in der extensions.conf (nur Auszug dargestell)
[telefone]

; IAX2-Verbindung zu Rainer
exten => _8840X.,1,NoOp(IAX2 - Volker ruft Rainer an mit Vorwahl 8840)
same => n,Dial(IAX2/outgoing-rainer/${EXTEN:4}) ; hier steht das entscheidende outgoing-rainer
same => n,Gosub(hinweise3,s,1) ; kann man sich sparen, will nur eigene Warnmeldungen hören.
; https://das-asterisk-buch.de/1.6/applications-gotoif.html
; https://www.voip-info.org/asterisk-variable-dialstatus/

Die Sound-Dateien, welche das AGI-Skript aufruft:

Download aller Audiodateien als mp3 und wav: Audiodateien fuer externeprovider.py.zip

halloliste.wav:

waehlensie.wav:

verbindungsaufbau.wav:

fehler.wav:

Erzeugt wurden die Soundfiles mit https://ttsmp3.com/ und der deutschen Sprecherin Marlene. Die mp3-Dateien wurden dann auf https://g711.org/ mit der Voreinstellung (Standard Definition 16-bit WAV (8 kHz, Mono, 16-Bit PCM) in WAV-Dateien umgewandelt, die Asterisk verarbeiten kann. Die WAV-Dateien klingen etwas dumpfer als die MP3-Dateien.

Für die Spracherzeugung mit TTSMP3 habe ich mir den Text für die halloliste.wav abgespeichert:

Für TTSMP3 https://ttsmp3.com/ und deutsche Sprecherin Marlene:

Hallo und herzlich Willkommen! Bitte wählen Sie folgende Auswahlnummern,
um in die externen Telefonnetze zu gelangen:<break time="1s"/>
Si Star Net: Null Eins,<break time="1s"/> 
Kolzentrik: Null Zwei,<break time="1s"/>
Iptel: Null Drei,<break time="1s"/> 
SippTuSipp: Null Vier,<break time="1s"/> 
AntiSipp: Null Fünf,<break time="1s"/>
Dus Punkt Net: Null Sechs,<break time="1s"/>
Opensipps: Null Sieben,<break time="1s"/>
Voipfon: Null Acht,<break time="1s"/>
SipKohl Österreich: Null Neun,<break time="1s"/>
Ippi: Zehn,<break time="1s"/>
D L Eins H R C: Elf,<break time="1s"/>
D L Fünf P D: Zwölf,<break time="1s"/>

Dadurch sind Anpassungen bei der Änderung der Liste schnell erstellt. Man beachte die merkwürdige Schreibweise, damit die Aussprache der englischen Wörter halbwegs stimmt.

********************

20. Februar 2023

Nachfolgend die Version 2 des Auswahlmenüs:

Was hat sich geändert? Es sind mehr Provider für die Auswahl hinzugekommen. Es gibt jetzt noch eine Ansage mit weiteren Informationen unter 88 und nach dem Eintippen der Nummern erfolgt ein Aufsagen der gewählten Nummer Ziffer für Ziffer für die Kontrolle. Dafür wurde die Definition “saydigits” geschaffen. Dann erfolgt de Verbindungsaufbau. Die Ansagen wurden überarbeitet.

Abgesehen von den notwendigen Definitionen für die Kommunikation mit Asterisk kommen  drei Definitionen (def) zum Einsatz:

def getnumber: Holt sich die zweistelligen Auswahlnummern, die in die Tastatur als DTMF eingeben wurden.

def number1: Holt sich die eingetippten Telefonnummern, die in die Tastatur getippt wurden.

def saydigits: Sagt die eingetippte Nummer als Abfolge von Ziffern auf, bevor sie versendet wird.

Die def number1 ruft die beiden Definitionen def getnumber   und die def saydigits für eine if-Anweisung auf. Die def number1 wird wiederum in einer weiteren einer if-schleife verwendet.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Name der Datei: externeprovider.py
# 17:57 Montag, 20. Februar 2023 Version 2

######### Nachfolgendes erspart den Einsatz von pyst #######################

import sys
import re
import time

# Read and ignore AGI environment (read until blank line)

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 result
   else:
      sys.stderr.write("FAIL (unexpected result '%s')\n" % params)
      sys.stderr.flush()
      return -2

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)

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()
   checkresult(result)

def getnumber (prompt, timelimit, digcount):
   sys.stderr.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stderr.flush()
   sys.stdout.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   result = checkresult(result)
   sys.stderr.write("digits are %s\n" % result)
   sys.stderr.flush()
   if result:
      return result
   else:
      result = -1

################### Ab hier beginnt das eigentliche Skript #####################

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)

# Eingabe der Nummer eines externen Providers ohne Vorwahl


def number1(digitcountsnumber, ttanswersnumber):
    result1 = getnumber("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_waehlensie",ttanswersnumber,digitcountsnumber)
    if int(result1) < 0 :
        print("SET VARIABLE EXTNEW " + "5553") 
        sys.stdout.flush()
        print("SET VARIABLE CONTEXTNEW " + "telefone")
        sys.stdout.flush()
        sayit("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_fehler")
        time_duration = 4
        time.sleep(time_duration)
    else:
        print("SET VARIABLE EXTNEW " + str(result1))
        sys.stdout.flush() # ohne flush hoert man das nachfolgende soundfile nicht
        sayit("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_nummer")
        time_duration = 3                                                                  # neu
        time.sleep(time_duration)  
        saydigits(result1)
        sys.stdout.flush()
        sayit("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_verbindungsaufbau")
        time_duration = 2
        time.sleep(time_duration)

limit=5         # nicht definiert
digitcount=2    # Der Nutzer kann nur max. 2 Ziffern eingeben und danach beginnt die Vermittlung
ttanswer=5000   # Wenn nach 5000 ms nichts mehr eingeben wird, ist die Zeit überschritten

digitcountsnumber = 50  # Maximal 50 Ziffern sind erlaubt bei Eingabe der Nummer
ttanswersnumber = 8000  # Wenn 8000 ms nichts mehr eingeben wird, ist die Zeit überschritten


time_duration = 1           # Ein Sekunde Pause eingebaut, damit der Nutzer den Anfang
time.sleep(time_duration)   # der Ansage mitbekommt.

# Nachfolgend erfolgt das Abspielen eines WAV- oder GSM-Soundfiles, dass den Nutzer
# informiert welche Ziffer er zu wählen hat. Der Nutzer kann während des Abspielens
# der Sounddatei die gewünschte Ziffer wählen. Sogleich wird er weiterverbunden.
# Die Sounddateien lassen sich z.B. mit Text to Speech auf https://ttsmp3.com/ als MP3 erstellen
# und anschließend auf https://g711.org/ mit der Standardeinstellung in das g.711-Format
# als Wav-Datei umwandeln, die für Asterisk  geeignet ist.

number = getnumber("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_halloliste",ttanswer,digitcount)

# number ist die Ziffer oder die Zahl, die vom Nutzer in die Tastatur des Telefons eingetippt
# wurde.

# In der if-elif-else Anweisung wird sie in eine Ganzzahl umgewandelt. Diese If-Aweisung
# bestimmt, welche Nummern (Extensions) gewählt werden, wenn man
# eine Ziffer oder Zahl eintippt.

# Tippt man die z.B. die 01 ein, dann wird mit Print ein String ausgedruckt,
# der die Variable CONTEXTNEW setzt. Das ist der Context für die Extension 
# des externen Providers

if int(number) == 1:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_cnet") 
elif int(number) == 2:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_callcentric") 
elif int(number) == 3:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_iptel") 
elif int(number) == 4:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_sip2sip") 
elif int(number) == 5:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_antisip") 
elif int(number) == 6:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dusnet")   
elif int(number) == 7:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_opensips")    
elif int(number) == 8:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_voipfone")  
elif int(number) == 9:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_sipcall")
elif int(number) == 10:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_ippi") 
elif int(number) == 11:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dl1hrc")  
elif int(number) == 12:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_dl5pd")
elif int(number) == 13:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_sipbroker") 
elif int(number) == 14:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_volkerstestserver") 
elif int(number) == 15:
    number1(digitcountsnumber,ttanswersnumber)
    print("SET VARIABLE CONTEXTNEW " + "goto_zadarma") 
elif int(number) == 88:
    print("SET VARIABLE EXTNEW " + "5553") # Diese Menue wird erneut angerufen und wiederholt sich
    sys.stdout.flush()
    print("SET VARIABLE CONTEXTNEW " + "telefone")
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/auswahlmenues/externeprovider_informationen")
    time_duration = 44
    time.sleep(time_duration)    
elif int(number) == 0: # Zurück zum Hautptmenue 5550 bei Wahl der Null
    print("SET VARIABLE EXTNEW " + "5550") 
    sys.stdout.flush()
    print("SET VARIABLE CONTEXTNEW " + "telefone")
    sys.stdout.flush()    
else: 
    sayit("/usr/share/asterisk/sounds/en_US_f_Allison/beep")
    time_duration = 2
    time.sleep(time_duration) 

# Wenn der Nutzer die 5 Sekunden verstreichen lässt oder eine
# nicht definierte Zahl eintippt, springt das Skript zu
# else, es wird eine Sounddatei abgespielt die Piep macht
# und dann geht es wieder von vorne los.

#
# exampel in extensions.conf
#
# [telefone]
# exten => 501,1,Answer() # Hier im Demo die 501 wählen, um ins Menue zu kommen
#  same => n,AGI(externeprovider.py) # eventuell absolute Pfadangabe notwendig
#  same => n,NoOP(from script externeprovider.py dialing ${CONTEXTNEW} , ${EXTNEW})
#  same => n,Goto(${CONTEXTNEW},${EXTNEW},1) # Hier geht es zum Context und zur Extension
#  same => n,Hangup()


# ; Fuer jeden externen Provider gibt es einen eigenen isolierten Context,
# , um Konflikte mit Wahlregeln zu vermeiden. Drei Beispiele:
#
# [goto_iptel] 
# exten => _X.,1,Dial(SIP/${EXTEN}@iptel,60,tT)

# [goto_sip2sip] 
# exten => _X.,1,Dial(SIP/${EXTEN}@sip2sip,60,tT)

# [goto_cnet]
# exten => _X.,1,Dial(IAX2/outgoing-cnet/${EXTEN})

Nachfolgend der Auszug aus der extensions.conf:

[goto_cnet] ; C*Net 01
exten => _X.,1,Dial(IAX2/outgoing-cnet/${EXTEN})

[goto_callcentric] ; Callcentric 02
exten => _X.,1,Dial(SIP/${EXTEN}@callcentric,60,tT)

[goto_iptel] ; Iptel 03
exten => _X.,1,Dial(SIP/${EXTEN}@iptel,60,tT)

[goto_sip2sip] ; Sip2Sip 04
exten => _X.,1,Dial(SIP/${EXTEN}@sip2sip,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_antisip] ; AntiSip 05
exten => _X.,1,Dial(SIP/${EXTEN}@antisip,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_dusnet] ; Dusnet 06
exten => _X.,1,Dial(SIP/${EXTEN}@dusnet,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_opensips] ; Opensips 07
exten => _X.,1,Dial(SIP/${EXTEN}@opensips,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_voipfone] ; Voipfone 08
exten => _X.,1,Dial(SIP/${EXTEN}@voipfone,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_sipcall] ; SipCall Österreich 09
exten => _X.,1,Dial(SIP/${EXTEN}@sipcall,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_ippi] ; Ippi 10
exten => _X.,1,Dial(SIP/${EXTEN}@ippi,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_dl1hrc] ; DL1HRC 11
exten => _X.,1,Dial(IAX2/outgoing-dl1hrc/${EXTEN})
same => n,Gosub(hinweise,s,1)

[goto_dl5pd] ; DL5PD 12
exten => _X.,1,Dial(IAX2/outgoing-rainer/${EXTEN})
same => n,Gosub(hinweise,s,1);

[goto_sipbroker] ; Sipbroker 13
exten => _X.,1,Dial(SIP/${EXTEN}@sipbroker,60,tT)
same => n,Gosub(hinweise,s,1)

[goto_volkerstestserver] ; Volkers Testserver Notebook 14
exten => _X.,1,Dial(IAX2/outgoing-lubuntu/${EXTEN})
same => n,Gosub(hinweise,s,1);

[goto_zadarma] ; Ippi 15
exten => _X.,1,Dial(SIP/${EXTEN}@zadarma,60,tT)
same => n,Gosub(hinweise,s,1)

Die neuen Soundfiles als Zip: Audio-Dateien-Externe-Provider-Version-2

Die Ansage für die Auswahlliste:

Texte für die Spracherzeugung mit https://ttsmp3.com/:

Hallo! Hier erhalten Sie Zugänge zu externen Telefonnetzen und angeschlossenen Providern.
Nach der Eingabe der zweistelligen Zugangsnummer werden Sie aufgefordert eine Telefonnummer des externen Netzes einzugeben. <break time="1s"/>

Sie Star Net:<prosody rate="slow">  0 1 </prosody><break time="1s"/>
Callcentric:<prosody rate="slow"> 0 2 </prosody><break time="1s"/>
Iptel:<prosody rate="slow"> 0 3 </prosody><break time="1s"/>
Sip tu Sip:<prosody rate="slow"> 0 4 </prosody><break time="1s"/>
Anti Sip:<prosody rate="slow"> 0 5 </prosody><break time="1s"/>
Dus Punkt Net:<prosody rate="slow"> 0 6 </prosody><break time="1s"/>
Open Sips:<prosody rate="slow"> 0 7 </prosody><break time="1s"/>
Voip Phone:<prosody rate="slow"> 0 8 </prosody><break time="1s"/>
Sip Call Österreich:<prosody rate="slow"> 0 9 </prosody><break time="1s"/>
Ippi:<prosody rate="slow"> 10 </prosody><break time="1s"/>
D L Eins H R C:<prosody rate="slow"> 11 </prosody><break time="1s"/>
D L Fünf P D:<prosody rate="slow"> 12 </prosody><break time="1s"/>
Sip Broker:<prosody rate="slow"> 13 </prosody><break time="1s"/>
Volkers Test Server:<prosody rate="slow"> 14 </prosody><break time="1s"/>
Zadarma:<prosody rate="slow">: 15 </prosody><break time="1s"/>
Informationen über den Zugang zu den externen Netzen:<prosody rate="slow"> 88 </prosody><break time="1s"/>
Zurück zum Hauptmenü:<prosody rate="slow"> 0 0 </prosody><break time="1s"/>

Die Telefonnummer können Sie übrigens mit der Raute-Taste abschließen, um den Wählvorgang zu beschleunigen.
Andernfalls verzögert sich der Verbindungsaufbau um mehrere Sekunden.
Die zweistelligen Auswahl-Nummern können Sie während der laufenden Ansage eingeben. 

Es erfolgt die Wiederholung der Ansage.

................................................................

externeprovider_informationen.wav

Die Möglichkeit sich mit externen Telefonnetzen verbinden zu lassen ist ein zentraler Bestandteil dieses Telefon-Servers. Diese Funktion ermöglicht Ihnen kostenlose Telefonate in den wichtigsten geschlossenen Telefonnetzen, ohne dass Sie sich dafür anmelden müssen. Wenn Sie wie üblich eine Flatrate für das deutsche Festnetz besitzen, können Sie zum Beispiel jede Telefonnummer von Iptel erreichen, die sich jeder kostenlos beschaffen kann. Umgekehrt ist Volkers Server auch über die Iptel-Nummer 88  0 0 73 erreichbar. Wenn Sie eine eigene Telefonnummer von Volkers Asterisk-Server besitzen, sind Sie nicht nur über Iptel erreichbar sondern über viele andere Telefonnetze.

Mit den abgespeicherten Texten lassen sich Änderungen schneller vornehmen.

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

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