27.12.2024
Dieses Python-basierte AGI-Skript ermöglicht es Asterisk-Telefonsystemen, die aktuelle Temperatur eines bestimmten Ortes in Deutschland abzufragen. Der Nutzer gibt eine 5-stellige deutsche Postleitzahl ein, und das Skript liefert die Temperatur für den entsprechenden Ort
Bedienungsanleitung: Um das Skript zu nutzen, wählen Sie laut Beispiel die Durchwahl 506 auf Ihrem Asterisk-Telefonsystem. Nach dem Wählen werden Sie aufgefordert, eine 5-stellige deutsche Postleitzahl einzugeben. Das Skript prüft Ihre Eingabe und bestätigt sie. Anschließend wird die aktuelle Temperatur für den Ort, der dieser Postleitzahl entspricht, angesagt.
Funktionsweise:
Nach der Eingabe der Postleitzahl wird diese durch eine Anfrage an OpenStreetMap in geographische Koordinaten (Breiten- und Längengrad) umgewandelt.
Mit diesen Koordinaten wird dann eine Anfrage an die met.no API gestellt, die die aktuelle Temperatur liefert.
Die Temperatur wird in Grad Celsius angesagt. Falls ein Fehler auftritt (z.B. eine ungültige Postleitzahl oder eine API-Anfragefehler), wird eine entsprechende Fehlermeldung ausgegeben.
Das Skript ist auf eine Eingabe von genau 5 Ziffern begrenzt, um sicherzustellen, dass nur gültige Postleitzahlen verarbeitet werden.
Das AGI-Skript in Python:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # WICHTIG FÜR API-ANMELDUNG!!!! #################################################################### # Ersetze im Skript "Max Mustermann/1.0 (max@mustermann.de)" durch # # deinen richtigen Namen und deine gültige E-Mail-Adresse # # Keine weiteren Schritte für die Anmeldung erforderlich. # #################################################################### # Datum: 27.12.2024 # Version: 1.0 # Autor: Volker Lange-Janson SM5ZBS # Dateiname: zipcode-dl-wy.py # AGI-Skript für Asterisk-Telefonsoftware, getestet auf Asterisk Version 18.xxx # Nach der Eingabe einer 5-stelligen deutschen Postleitzahl erfolgt die Ansage # der aktuellen Temperatur am Ort der Postleitzahl # Eingabe ist auf 5 Ziffern begrenzt # Aufruf des AGI-Skripts in der extensions.conf, wenn man 506 wählt. # exten => 506,1,Noop(Anruf erfolgt von: "${CALLERID(name)}" <${CALLERID(num)}>) # same => n,Noop(Aktuelle Temperaturabfrage mit deutschen Postleitzahlen) # same => n,Answer() # same => n,AGI(zipcode-dl-wx.py) # same => n,Hangup() import requests import sys import re import time # from functools import lru_cache # Nachfolgendes wird für die Kommunikation mit Asterisk benötigt: #################################################################### # Read the incoming AGI environment variables env = {} while True: line = sys.stdin.readline().strip() if line == '': break key, data = line.split(':', 1) if not key.startswith('agi_'): sys.stderr.write("Did not work!\n") sys.stderr.flush() continue env[key.strip()] = data.strip() sys.stderr.write("AGI Environment Dump:\n") sys.stderr.flush() for key, value in env.items(): sys.stderr.write(f" -- {key} = {value}\n") 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(f"FAIL ('{params}')\n") sys.stderr.flush() return -1 else: result = result.group(1) sys.stderr.write(f"PASS ({result})\n") 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(f"FAIL (unexpected result '{params}', error code {error_code})\n") sys.stderr.flush() return error_code else: sys.stderr.write(f"FAIL (unexpected result '{params}')\n") sys.stderr.flush() return -2 def saynumber(params): sys.stderr.write(f"SAY NUMBER {params} \"\"\n") sys.stderr.flush() sys.stdout.write(f"SAY NUMBER {params} \"\"\n") sys.stdout.flush() result = sys.stdin.readline().strip() return checkresult(result) def sayit(params): sys.stderr.write(f"STREAM FILE {params} \"\"\n") sys.stderr.flush() sys.stdout.write(f"STREAM FILE {params} \"\"\n") sys.stdout.flush() result = sys.stdin.readline().strip() checkresult(result) def getnumber(prompt, timelimit, digcount): sys.stderr.write(f"GET DATA {prompt} {timelimit} {digcount}\n") sys.stderr.flush() sys.stdout.write(f"GET DATA {prompt} {timelimit} {digcount}\n") sys.stdout.flush() result = sys.stdin.readline().strip() return checkresult(result) ################################################################## # Hier fängt das eigentliche Programm an def say_digits(number): """ Sagt jede Ziffer einer Zahl einzeln. """ for digit in str(number): saynumber(digit) limit = 60 digitcount = 5 score = 0 count = 0 ttanswer = 20000 postcode = getnumber("/usr/share/asterisk/sounds/eigene/plztemps/zipcode", ttanswer, digitcount) sayit("/usr/share/asterisk/sounds/eigene/plztemps/you-entered-zipcode") say_digits(postcode) sayit("/usr/share/asterisk/sounds/eigene/plztemps/irepeat") say_digits(postcode) # Funktionen zur API-Nutzung # @lru_cache(maxsize=128) def get_coordinates_from_postcode(postcode): """ Wandelt eine deutsche Postleitzahl in Koordinaten um, basierend auf Nominatim (OpenStreetMap). """ url = "https://nominatim.openstreetmap.org/search" params = { "postalcode": postcode, "country": "Germany", "format": "json" } headers_osm = { "User-Agent": "Max Mustermann/1.0 (max@mustermann.de)" } response = requests.get(url, params=params, headers=headers_osm) response.raise_for_status() results = response.json() if results: lat = results[0]["lat"] lon = results[0]["lon"] return float(lat), float(lon) else: raise ValueError("Keine Ergebnisse für diese Postleitzahl gefunden!") def get_weather(lat, lon): """ Holt die Wetterdaten von der API von met.no """ url = f"https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={lat}&lon={lon}" headers_metno = { "User-Agent": "AktuelleTemperaturDeutschePostleitzahl/1.0 (janson@janson-soft.de)" } response = requests.get(url, headers=headers_metno) response.raise_for_status() data = response.json() temperature = data["properties"]["timeseries"][0]["data"]["instant"]["details"]["air_temperature"] return round(temperature) try: # Koordinaten aus der Postleitzahl berechnen lat, lon = get_coordinates_from_postcode(postcode) # Temperatur von der API holen temperature = get_weather(lat, lon) # Ausgabe der Temperatur sayit("/usr/share/asterisk/sounds/eigene/plztemps/temp") saynumber(temperature) sayit("/usr/share/asterisk/sounds/eigene/plztemps/irepeat") saynumber(temperature) sayit("/usr/share/asterisk/sounds/eigene/plztemps/irepeat") saynumber(temperature) except ValueError as e: sys.stderr.write(f"Eingabefehler: {e}\n") sayit("/usr/share/asterisk/sounds/eigene/plztemps/error1") except requests.RequestException as e: sys.stderr.write(f"Fehler bei der API-Anfrage: {e}\n") sayit("/usr/share/asterisk/sounds/eigene/plztemps/error2") except KeyError as e: sys.stderr.write(f"Fehler beim Verarbeiten der API-Daten: {e}\n") sayit("/usr/share/asterisk/sounds/eigene/plztemps/error3")
Aufruf in der extensions.conf: Im Beispiel ist die 506 zu wählen.
exten => 506,1,Noop(Anruf erfolgt von: "${CALLERID(name)}" <${CALLERID(num)}>) same => n,Noop(Abfrage aktueller Temperaturen mit deutschen Postleitzahlen) same => n,Answer() same => n,AGI(zipcode-dl-wx.py) same => n,Hangup()
Was zeigt die Asterisk-Konsole beim Ausführen des Skripts? Nachfolgend die Ausgabe in der Asterisk-Konsole, wenn eine Nummer (von einem anderen Server über IAX2) die 506 anwählt, um das AGI-Skript auszuführen. Damit man den vollen Umfang der Informationen erhält, ist in der Konsole
agi set debug on
zu setzen. Mit „agi set debug off“ schalten wir diesen Modus wieder ab.
Accepting AUTHENTICATED call from 192.168.1.111:4569: -- > requested format = alaw, -- > requested prefs = (alaw|g722|ulaw|gsm), -- > actual format = g722, -- > host prefs = (g722|alaw|ulaw|gsm), -- > priority = mine -- Executing [506@telefone:1] NoOp("IAX2/incoming-test77-11708", "Anruf erfolgt von: "Volker Lange-Janson" <1088>") in new stack -- Executing [506@telefone:2] Answer("IAX2/incoming-test77-11708", "") in new stack -- Executing [506@telefone:3] AGI("IAX2/incoming-test77-11708", "zipcode-dl-wx.py") in new stack -- Launched AGI Script /usr/share/asterisk/agi-bin/zipcode-dl-wx.py <IAX2/incoming-test77-11708>AGI Tx >> agi_request: zipcode-dl-wx.py <IAX2/incoming-test77-11708>AGI Tx >> agi_channel: IAX2/incoming-test77-11708 <IAX2/incoming-test77-11708>AGI Tx >> agi_language: en <IAX2/incoming-test77-11708>AGI Tx >> agi_type: IAX2 <IAX2/incoming-test77-11708>AGI Tx >> agi_uniqueid: 1735322486.72 <IAX2/incoming-test77-11708>AGI Tx >> agi_version: 18.10.0~dfsg+~cs6.10.40431411-2 <IAX2/incoming-test77-11708>AGI Tx >> agi_callerid: 1088 <IAX2/incoming-test77-11708>AGI Tx >> agi_calleridname: Volker Lange-Janson <IAX2/incoming-test77-11708>AGI Tx >> agi_callingpres: 0 <IAX2/incoming-test77-11708>AGI Tx >> agi_callingani2: 0 <IAX2/incoming-test77-11708>AGI Tx >> agi_callington: 0 <IAX2/incoming-test77-11708>AGI Tx >> agi_callingtns: 0 <IAX2/incoming-test77-11708>AGI Tx >> agi_dnid: unknown <IAX2/incoming-test77-11708>AGI Tx >> agi_rdnis: unknown <IAX2/incoming-test77-11708>AGI Tx >> agi_context: telefone <IAX2/incoming-test77-11708>AGI Tx >> agi_extension: 506 <IAX2/incoming-test77-11708>AGI Tx >> agi_priority: 3 <IAX2/incoming-test77-11708>AGI Tx >> agi_enhanced: 0.0 <IAX2/incoming-test77-11708>AGI Tx >> agi_accountcode: <IAX2/incoming-test77-11708>AGI Tx >> agi_threadid: 130098168591936 <IAX2/incoming-test77-11708>AGI Tx >> <IAX2/incoming-test77-11708>AGI Rx << GET DATA /usr/share/asterisk/sounds/eigene/plztemps/zipcode 20000 5 -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/zipcode.slin' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=24534 <IAX2/incoming-test77-11708>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/eigene/plztemps/you-entered-zipcode "" -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/you-entered-zipcode.slin' (escape_digits=) (sample_offset 0) (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 endpos=20925 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 2 "" -- <IAX2/incoming-test77-11708> Playing 'digits/2.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 4 "" -- <IAX2/incoming-test77-11708> Playing 'digits/4.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 5 "" -- <IAX2/incoming-test77-11708> Playing 'digits/5.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 3 "" -- <IAX2/incoming-test77-11708> Playing 'digits/3.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 4 "" -- <IAX2/incoming-test77-11708> Playing 'digits/4.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/eigene/plztemps/irepeat "" -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/irepeat.slin' (escape_digits=) (sample_offset 0) (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 endpos=9021 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 2 "" -- <IAX2/incoming-test77-11708> Playing 'digits/2.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 4 "" -- <IAX2/incoming-test77-11708> Playing 'digits/4.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 5 "" -- <IAX2/incoming-test77-11708> Playing 'digits/5.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 3 "" -- <IAX2/incoming-test77-11708> Playing 'digits/3.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 4 "" -- <IAX2/incoming-test77-11708> Playing 'digits/4.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/eigene/plztemps/temp "" -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/temp.slin' (escape_digits=) (sample_offset 0) (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 endpos=39165 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 5 "" -- <IAX2/incoming-test77-11708> Playing 'digits/5.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/eigene/plztemps/irepeat "" -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/irepeat.slin' (escape_digits=) (sample_offset 0) (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 endpos=9021 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 5 "" -- <IAX2/incoming-test77-11708> Playing 'digits/5.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 <IAX2/incoming-test77-11708>AGI Rx << STREAM FILE /usr/share/asterisk/sounds/eigene/plztemps/irepeat "" -- <IAX2/incoming-test77-11708> Playing '/usr/share/asterisk/sounds/eigene/plztemps/irepeat.slin' (escape_digits=) (sample_offset 0) (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 endpos=9021 <IAX2/incoming-test77-11708>AGI Rx << SAY NUMBER 5 "" -- <IAX2/incoming-test77-11708> Playing 'digits/5.gsm' (language 'en') <IAX2/incoming-test77-11708>AGI Tx >> 200 result=0 -- <IAX2/incoming-test77-11708>AGI Script zipcode-dl-wx.py completed, returning 0 -- Executing [506@telefone:4] Hangup("IAX2/incoming-test77-11708", "") in new stack == Spawn extension (telefone, 506, 4) exited non-zero on 'IAX2/incoming-test77-11708' -- Hungup 'IAX2/incoming-test77-11708'
Erklärung der verwendeten Webdienste: Das Skript greift auf zwei zentrale Webdienste zu, um die Funktionalität zur Temperaturabfrage basierend auf deutschen Postleitzahlen bereitzustellen:
Nominatim API (OpenStreetMap): Die Nominatim API von OpenStreetMap wird genutzt, um die geografischen Koordinaten (Breitengrad und Längengrad) aus einer deutschen Postleitzahl zu ermitteln.
Anfrage: Die Postleitzahl wird zusammen mit dem Ländercode („Germany“) an die API übergeben.
Ergebnis: Die API liefert die Koordinaten des zugehörigen geografischen Ortes.
Met.no Wetter-API (Norwegian Meteorological Institute): Mit den ermittelten Koordinaten fragt das Skript die Wetter-API von met.no ab.
Anfrage: Die Koordinaten werden als Parameter übergeben.
Ergebnis:Die API liefert detaillierte Wetterdaten, einschließlich der aktuellen Lufttemperatur, die dann im Telefonsystem angesagt wird.
Hinweis: Beide APIs erfordern keine kostenpflichtige Registrierung, jedoch ist es notwendig, für jede Anfrage einen individuellen User-Agent-Header anzugeben. Dieser enthält beispielsweise Deinen Namen oder Projektinformationen und dient der Identifikation und dem verantwortungsvollen Umgang mit den Diensten.
Sounddateien: Sie liegen in US-Englisch vor und wurden mit https://ttsmp3.com/ (Johanna US-English) als MP3 erzeugt und anschließend mit der Voreinstellung von https://g711.org/ umgewandelt.
Sounddateien Englisch Temperaturabfrage mit deutschen Postleitzahlen.zip
Laut Skript gehören die Sounddateien in den Pfad /usr/share/asterisk/sounds/eigene/plztemps/.
************
Wie ist dieses Skript entstanden? Die ursprüngliche Idee stammt von ChatGPT, das dieses Skript auf meine Anregung hin vorstellte:
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Ersetze Max Mustermann und max@mustermann.de durch deinen Namen # und deine eigen E-Mail-Adresse # für die API-Anmeldung import requests import time def get_coordinates_from_postcode(postcode): """ Wandelt eine deutsche Postleitzahl in Koordinaten um, basierend auf Nominatim (OpenStreetMap). """ url = "https://nominatim.openstreetmap.org/search" params = { "postalcode": postcode, "country": "Germany", "format": "json" } headers_osm = { "User-Agent": "Max Mustermann/1.0 (max@mustermann.de)" # Benutzerdefinierter User-Agent für Nominatim } response = requests.get(url, params=params, headers=headers_osm) response.raise_for_status() results = response.json() if results: lat = results[0]["lat"] lon = results[0]["lon"] return float(lat), float(lon) else: raise ValueError("Keine Ergebnisse für diese Postleitzahl gefunden!") def get_weather(lat, lon): """ Holt die Wetterdaten von der API von met.no """ url = f"https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={lat}&lon={lon}" headers_metno = { "User-Agent": "AktuelleTemperaturDeutschePostleitzahl/1.0 (max@mustermann.de)" # User-Agent für met.no } response = requests.get(url, headers=headers_metno) response.raise_for_status() data = response.json() temperature = data["properties"]["timeseries"][0]["data"]["instant"]["details"]["air_temperature"] return round(temperature) try: # Nutzer gibt Postleitzahl ein postcode = input("Gib die deutsche Postleitzahl ein: ") # Koordinaten aus der Postleitzahl berechnen lat, lon = get_coordinates_from_postcode(postcode) # Temperatur von der API holen temperature = get_weather(lat, lon) # Ausgabe der Temperatur print(f"Die aktuelle Temperatur in {postcode} beträgt {temperature} °C.") except ValueError as e: print(f"Eingabefehler: {e}") except requests.RequestException as e: print(f"Fehler bei der API-Anfrage: {e}") except KeyError as e: print(f"Fehler beim Verarbeiten der API-Daten: {e}")
Es ist für die Eingabe und Ausgabe auf der Kommandozeilenebene ausgelegt und läuft z.B. auf Ubuntu mit Thonny.
Damit es als AGI für Asterisk läuft, habe ich die als Beispiel ausgelegte Anleitung auf
http://www.asteriskdocs.org/en/2nd_Edition/asterisk-book-html-chunk/asterisk-CHP-9-SECT-4.html
herangezogen. Mit diesem Beispiel wird erklärt, wie die Eingabe von Nummern über das Tastenfeld des Telefons und die Sprachausgabe von Nummern, Ziffern und Sounddateien in Python verarbeitet wird. Mit etwas Probieren gelang dann die Anpassung. Das habe ich selbst ausprobieren müssen. ChatGPT habe ich dann nochmals den Code untersuchen lassen und baute ein paar Sicherheitsmaßnahmen ein. Diese führten dazu, dass andere laufende Gespräche abgebrochen wurden. Deshalb enfernte ich diese Sicherheitsmaßnahmen wieder.
Hilfestellung durch ChatGPT: ChatGPT hat mir viel Arbeit abgenommen. Dennoch hat es einige Stunden gedauert, bis das Programm so lief, wie ich es mir vorstellte. Bei der Erstellung der Sounddateien hat mir ChatGPT mit den Übersetzungen geholfen. Der Text dieser Seite stammt zu 70% von ChatGPT. Und das witzige Bild hat mir ChatGPT ebenfalls erstellt. Zum Einsatz kam die kostenlose Version von ChatGPT. Vor zwei Jahren war diese KI bei weitem nicht so leistungsfähig und wir können gespannt auf die zukünftige Entwicklung sein, denn die Künstliche Intelligenz befindet sich immer noch in den Kinderschuhen.
Man muss auch bedenken, dass man mit solch ausgefallenen Themen kein Geld verdienen kann. Nur sehr wenige interessieren sich für Asterisk. Noch weniger interessieren sich für die AGI-Programmierung für Asterisk. Durch die zahlreichen Messengerdienste schwindet das Interesse an Telefonsoftware. Das ist bedauerlich. Moderne DECT-Telefone sind bequem in der Handhabung und bieten eine hervorragende Tonqualität selbst mit einer Freisprecheinrichtung.
Weiterführende Informationen zu Asterisk und AGI:
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 – |