Weltweite Wetterdaten der WWIS-Webseite mit Python auslesen und als AGI-Skript in Asterisk einbinden

27.03.2023

Hier wird schrittweise erklärt, wie sich weltweite Wetterdaten der WWIS-Webseite mit Python auslesen lassen. Dabei hat mir ChatGPT geholfen und viel Sucherei erspart. Wichtig ist beim Umgang mit ChatGPT eine stufenweise Herangehensweise an die Aufgabenstellung. ChatGPT ist eine große Hilfe. Allerdings sind einigermaßen gute Kenntnisse in Python notwendig, um die Fragen verständlich formulieren zu können. Schrittweise musste das Skript für eine englischsprachige Sprachausgabe vorbereitet werden, wobei zu beachten ist, dass die Sprachausgabe nur die Null und positive Ganzzahlen aufsagen kann.

Das Ziel ist es ein Python-Skript als AGI-Skript für Asterisk zu schreiben, dass die Temperaturen der schwedischen Stadt Norrköping ansagt. Ich habe Norrköping gewählt, weil das die nächste Stadt ist, die in der Liste der WWIS-Webseite aufgelistet ist. Selbstverständlich lässt sich das Skript leicht für andere Städte anpassen.

Auf https://www.wwis.dwd.de/de/dataguide.html befindet sich eine rudimentäre Anleitung, wie man die Daten der WWIS-Webseite erhält. Da ich mich in diesem Beispiel für die Wetterdaten der schwedischen Stadt Norrköping interessiere, muss ich auf https://worldweather.wmo.int/de/json/full_city_list.txt nach Norrköping suchen und erfahre das “1043” für Norrköping steht. Die Wetterdaten für Norrköping stehen demnach auf https://www.wwis.dwd.de/de/json/1043_de.xml. Diese Seite zeigt allerdings nur einen Hinweis an:

This page contains the following errors:
error on line 1 at column 1: Document is empty
Below is a rendering of the page up to the first error.

Was nun? Wir müssen den Seitenquelltext der Seite betrachten. Mit dem Webbrowser Chrome geht das zum Beispiel Mauszeiger auf beliebiger Stelle der Webseite, rechte Maustaste,  “Seitenquelltext anzeigen” anklicken. Wir sehen dann auf einer einzigen Zeile eine sehr lange Zeichenkette (String), den wir für die weitere Untersuchung am besten herauskopieren.

Wir wollen uns erst einmal mit den heutigen Minimal- und Maximaltemperaturen von Norrköping beschäftigen. Wir finden diese Werte ziemlich am Anfang des Strings. Deshalb meine Anfrage an ChatCPT:

“Die Seite https://www.wwis.dwd.de/de/json/1043_de.xml besteht aus einem einzigen String. Bitte suche aus diesem String die erste Zeichenabfolge “minTemp”:” und lege die Zahl dahinter in die Variable min_temp ab. Dann suche weiter, bis du du die erste Zeichenabfolge “maxTemp”:” findest und und lege die Zahl dahinter in die Variable max_temp ab.”

Antwort von ChatGPT: “Gerne helfe ich Ihnen bei dieser Aufgabe. Hier ist das Python-Skript, das die Zeichenabfolgen “minTemp” und “maxTemp” aus der Webseite https://www.wwis.dwd.de/de/json/1043_de.xml sucht und die entsprechenden Temperaturen in den Variablen “min_temp” und “max_temp” speichert:

import requests

url = "https://www.wwis.dwd.de/de/json/187_de.xml"

response = requests.get(url)
xml_content = response.text

min_temp_start = xml_content.find('"minTemp":"') + len('"minTemp":"')
min_temp_end = xml_content.find('"', min_temp_start)
min_temp = float(xml_content[min_temp_start:min_temp_end])

max_temp_start = xml_content.find('"maxTemp":"') + len('"maxTemp":"')
max_temp_end = xml_content.find('"', max_temp_start)
max_temp = float(xml_content[max_temp_start:max_temp_end])

print("Die minimale Temperatur beträgt", min_temp, "Grad Celsius.")
print("Die maximale Temperatur beträgt", max_temp, "Grad Celsius.")

Erklärung:

    1. Wir importieren das Python-Modul “requests”.
    2. Wir speichern die URL der Webseite in der Variablen “url”.
    3. Wir senden eine GET-Anfrage an die URL und speichern die Antwort in der Variablen “xml_content”.
    4. Wir suchen die Position des Texts “minTemp” in der Antwort und speichern die Position des ersten Zeichens nach “minTemp” in der Variablen “min_temp_start”.
    5. Wir suchen die Position des nächsten doppelten Anführungszeichens nach “minTemp” und speichern die Position in der Variablen “min_temp_end”.
    6. Wir extrahieren den Text zwischen den Positionen “min_temp_start” und “min_temp_end” aus der Antwort, konvertieren ihn in eine Gleitkommazahl und speichern ihn in der Variablen “min_temp”.
    7. Wir wiederholen Schritte 4 bis 6 für “maxTemp” und speichern das Ergebnis in der Variablen “max_temp”.
    8. Wir geben die minimale und maximale Temperatur aus.

Ich hoffe, das hilft!”

Es kam allerdings bei der ersten  Ausführung des Skripts folgende Fehlermeldung:  import requests ModuleNotFoundError: No module named ‘requests’ was nun? Die Fehlermeldung “ModuleNotFoundError: No module named ‘requests'” deutet darauf hin, dass das Python-Modul “requests” nicht installiert ist. Sie müssen das “requests”-Modul installieren, bevor Sie das Skript ausführen können.

Hier sind die Schritte, um das “requests”-Modul zu installieren:

    1. Öffnen Sie die Windows-Eingabeaufforderung.
    2. Geben Sie den Befehl “pip install requests” ein und drücken Sie die Eingabetaste.
    3. Warten Sie, bis das “requests”-Modul heruntergeladen und installiert ist.

Nach der Installation sollten Sie in der Lage sein, das Skript auszuführen, ohne die Fehlermeldung zu erhalten.

“pip install requests” wird ausgeführt.

Da später eine Sprachausgabe geplant ist, die nur positive Ganzzahlen und die Null aufsagen kann, muss die Ausgabe noch schrittweise angepasst werden. Meine Bitte an ChatGPT:  “Ich brauche noch eine Änderung der Ausgabe mit einer Fallunterscheidung, bei der das Minus-Zeichen (z.B. -4) ausgewertet wird, dass zum Beispiel folgende Ausgabe erfolgt, je nach dem ob, die Zahl negativ, positiv oder eine Null ist. Also wie folgt:

Ist die Zahl min_temp negativ: print(“Die minimale Temperatur beträgt minus”, min_temp, “Grad Celsius.”)

Ist die Zahl min_temp O: print(“Die minimale Temperatur beträgt”, min_temp, “Grad Celsius.”)

Ist die Zahl min_temp positiv: print(“Die maximale Temperatur beträgt plus”, min_temp, “Grad Celsius.”)

Das gleiche Muster bei max_temp: Ist die Zahl max_temp negativ: print(“Die maximale Temperatur beträgt minus”, min_temp, “Grad Celsius.”) Ist die Zahl max_temp O: print(“Die maximal Temperatur beträgt”, min_temp, “Grad Celsius.”) Ist die Zahl max_temp positiv: print(“Die maximale Temperatur beträgt plus”, min_temp, “Grad Celsius.”)”.

Antwort: Klar, gerne. Hier ist das aktualisierte Skript:

import requests

# Daten von der Webseite abrufen
url = "https://www.wwis.dwd.de/de/json/187_de.xml"
response = requests.get(url)
data = response.text

# Minimale Temperatur extrahieren
min_temp_start = data.find('"minTemp":"') + len('"minTemp":"')
min_temp_end = data.find('",', min_temp_start)
min_temp = int(data[min_temp_start:min_temp_end])

# Maximale Temperatur extrahieren
max_temp_start = data.find('"maxTemp":"') + len('"maxTemp":"')
max_temp_end = data.find('",', max_temp_start)
max_temp = int(data[max_temp_start:max_temp_end])

# Ausgabe mit Fallunterscheidung
if min_temp < 0:
    print("Die minimale Temperatur beträgt minus", abs(min_temp), "Grad Celsius.")
elif min_temp == 0:
    print("Die minimale Temperatur beträgt", min_temp, "Grad Celsius.")
else:
    print("Die minimale Temperatur beträgt plus", min_temp, "Grad Celsius.")

if max_temp < 0:
    print("Die maximale Temperatur beträgt minus", abs(max_temp), "Grad Celsius.")
elif max_temp == 0:
    print("Die maximale Temperatur beträgt", max_temp, "Grad Celsius.")
else:
    print("Die maximale Temperatur beträgt plus", max_temp, "Grad Celsius.")

Diese Skript liefert allerdings als Ergebnis Fließkommazahlen. Die Webseite liefert nur Ganzzahlen. Für die die spätere Sprachausgabe sind Ganzzahlen notwendig. Hier das angepasste Skript, das Ganzzahlen liefert:

import requests

url = "https://www.wwis.dwd.de/de/json/1043_de.xml"   # Norrköping
response = requests.get(url)
xml_content = response.text

min_temp_start = xml_content.find('"minTemp":"') + len('"minTemp":"')
min_temp_end = xml_content.find('"', min_temp_start)
min_temp = int(float(xml_content[min_temp_start:min_temp_end]))

max_temp_start = xml_content.find('"maxTemp":"') + len('"maxTemp":"')
max_temp_end = xml_content.find('"', max_temp_start)
max_temp = int(float(xml_content[max_temp_start:max_temp_end]))

print("Die minimale Temperatur in Norrköping beträgt heute", min_temp, "Grad Celsius.")
print("Die maximale Temperatur in Norrköping beträgt heute", max_temp, "Grad Celsius.")

# Ausgabe mit Fallunterscheidung
if min_temp < 0:
    print("Die minimale Temperatur beträgt minus", abs(min_temp), "Grad Celsius.")
elif min_temp == 0:
    print("Die minimale Temperatur beträgt", min_temp, "Grad Celsius.")
else:
    print("Die minimale Temperatur beträgt plus", min_temp, "Grad Celsius.")

if max_temp < 0:
    print("Die maximale Temperatur beträgt minus", abs(max_temp), "Grad Celsius.")
elif max_temp == 0:
    print("Die maximale Temperatur beträgt", max_temp, "Grad Celsius.")
else:
    print("Die maximale Temperatur beträgt plus", max_temp, "Grad Celsius.")

Hier wurde die Umwandlung in Ganzzahlen durch das Einfügen von int(float(...)) umgesetzt. Dabei wird der Text der Temperaturzahl zuerst in eine Float-Zahl konvertiert, um auch Werte mit Dezimalpunkt korrekt zu behandeln. Anschließend wird diese Float-Zahl in eine Ganzzahl umgewandelt, wobei die Nachkommastellen einfach abgeschnitten werden.

Jetzt wollte ich noch die Ausgabe auf Englisch und aus grammatikalischen Gründen muss auch die Ausgabe für die “1” gesondert behandelt werden:

if min_temp < 0:
    print("The minimum temperature is minus", abs(min_temp), "degrees Celsius.")
elif min_temp == 0:
    print("The minimum temperature is", min_temp, "degrees Celsius.")
elif min_temp == 1:
    print("The minimum temperature is one degree Celsius.")
else:
    print("The minimum temperature is plus", min_temp, "degrees Celsius.")

if max_temp < 0:
    print("The maximum temperature is minus", abs(max_temp), "degrees Celsius.")
elif max_temp == 0:
    print("The maximum temperature is", max_temp, "degrees Celsius.")
elif max_temp == 1:
    print("The maximum temperature is one degree Celsius.")
else:
    print("The maximum temperature is plus", max_temp, "degrees Celsius.")

Daraus ist nun ein vollständiges Skript zu montieren:

import requests

url = "https://www.wwis.dwd.de/de/json/1043_de.xml"   # Norrköping
response = requests.get(url)
xml_content = response.text

min_temp_start = xml_content.find('"minTemp":"') + len('"minTemp":"')
min_temp_end = xml_content.find('"', min_temp_start)
min_temp = int(float(xml_content[min_temp_start:min_temp_end]))

max_temp_start = xml_content.find('"maxTemp":"') + len('"maxTemp":"')
max_temp_end = xml_content.find('"', max_temp_start)
max_temp = int(float(xml_content[max_temp_start:max_temp_end]))



if min_temp < 0:
    min_temp = abs(min_temp)
    print("The minimum temperature is minus", min_temp, "degrees Celsius.")
elif min_temp == 0:
    print("The minimum temperature is", min_temp, "degrees Celsius.")
elif min_temp == 1:
    print("The minimum temperature is one degree Celsius.")
else:
    print("The minimum temperature is plus", min_temp, "degrees Celsius.")

if max_temp < 0:
    max_temp = abs(max_temp)
    print("The maximum temperature is minus", max_temp, "degrees Celsius.")
elif max_temp == 0:
    print("The maximum temperature is", max_temp, "degrees Celsius.")
elif max_temp == 1:
    print("The maximum temperature is one degree Celsius.")
else:
    print("The maximum temperature is plus", max_temp, "degrees Celsius.")

Ein Test ergab, dass die Ausgabe funktioniert:

The minimum temperature is minus 3 degrees Celsius.
The maximum temperature is one degree Celsius.
>>>

Nun kommt als Fleißarbeit die Erzeugung der Soundfiles auf  https://ttsmp3.com/:

https://ttsmp3.com/ US English / Salli

<prosody rate="slow">Here you can get the temperatures of Norrshöping in Sweden:</prosody>
<prosody rate="slow">Today's temperatures:</prosody>

<prosody rate="slow">degrees Celsius.</prosody>

<prosody rate="slow">The minimum temperature is minus</prosody>
<prosody rate="slow">The minimum temperature is</prosody>
<prosody rate="slow">The minimum temperature is one degree Celsius.</prosody>
<prosody rate="slow">The minimum temperature is plus</prosody>

<prosody rate="slow">The maximum temperature is minus</prosody>
<prosody rate="slow">The maximum temperature is</prosody>
<prosody rate="slow">The maximum temperature is one degree Celsius.</prosody>
<prosody rate="slow">The maximum temperature is plus</prosody>

Das sind immerhin 11 Soundfiles, die benötigt werden. Nachfolgend drei Beispiele davon:

Die 11 MP3-Soundfiles: Soundfiles-MP3-Norrkoeping.zip

Allerdings verwende ich auf Asterisk keine MP3-Files, sondern wandle sie in WAV-Files um. Dies erfolgt mit Wavepad von NCH Software:

Wavepad von NCH Software.

Die im Screenshot sichtbaren Einstellungen sind zu wählen. Das Ergebnis sind WAV-Dateien: Norrkoeping-WAV-Dateien.zip

Der Frequenzgang ist jetzt eingeschränkt und in Telefonqualität. Hier ein Beispiel:

Weiter geht es mit der Fleißarbeit, um aus dem vorhandenen Skript ein AGI-Skript zu machen, in dem die 11 Soundfiles einzubinden sind:

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

# Montag 27. März 2023 temp-norrkoeping.py Volker Lange-Janson

import requests
import sys
import re
import time

# Nachfolgendes wird für die Kommunikation mit Asterisk benötigt:
####################################################################
# 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

      
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 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)

##################################################################
# Hier fängt das eigentliche Programm an

time_duration = 1
time.sleep(time_duration) 

sayit("/usr/share/asterisk/sounds/eigene/wetter/here-get-temp-norrkoeping")
sys.stdout.flush()
time_duration = 1
time.sleep(time_duration)
sayit("/usr/share/asterisk/sounds/eigene/wetter/todays-temp")
sys.stdout.flush()
time_duration = 1
time.sleep(time_duration)

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

url = "https://www.wwis.dwd.de/de/json/1043_de.xml"   # Norrköping
response = requests.get(url)
xml_content = response.text

min_temp_start = xml_content.find('"minTemp":"') + len('"minTemp":"')
min_temp_end = xml_content.find('"', min_temp_start)
min_temp = int(float(xml_content[min_temp_start:min_temp_end]))

max_temp_start = xml_content.find('"maxTemp":"') + len('"maxTemp":"')
max_temp_end = xml_content.find('"', max_temp_start)
max_temp = int(float(xml_content[max_temp_start:max_temp_end]))

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

if min_temp < 0:
    min_temp = abs(min_temp)
    sayit("/usr/share/asterisk/sounds/eigene/wetter/min-temp-minus")
    sys.stdout.flush()
    saynumber(min_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
elif min_temp == 0:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/min-temp")
    sys.stdout.flush()
    saynumber(min_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
elif min_temp == 1:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/min-temp-one")
    sys.stdout.flush()
else:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/min-temp-plus")
    sys.stdout.flush()
    saynumber(min_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
    
time_duration = 1
time.sleep(time_duration)
    
if max_temp < 0:
    max_temp = abs(max_temp)
    sayit("/usr/share/asterisk/sounds/eigene/wetter/max-temp-minus")
    sys.stdout.flush()
    saynumber(max_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
elif max_temp == 0:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/max-temp")
    sys.stdout.flush()
    saynumber(max_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
elif max_temp == 1:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/max-temp-one")
    sys.stdout.flush()
else:
    sayit("/usr/share/asterisk/sounds/eigene/wetter/max-temp-plus")
    sys.stdout.flush()
    saynumber(max_temp);
    sys.stdout.flush()
    sayit("/usr/share/asterisk/sounds/eigene/wetter/degrees-celsius")
    sys.stdout.flush()
    

time_duration = 2
time.sleep(time_duration)


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

time_duration = 1
time.sleep(time_duration) 

sayit("/usr/share/asterisk/sounds/en_US_f_Allison/goodbye")
sys.stdout.flush()
time_duration = 1
time.sleep(time_duration)

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

# Beispiel, wie das Skript in der extensions.conf aufgerufen wird
# durch die Wahl der 351
#
# exten => 351,1,Noop(Wetter in Norrköping: "${CALLERID(name)}" <${CALLERID(num)}>) 
#  same => n,Answer()    
#  same => n,AGI(temp-norrkoeping.py)
#  same => n,Hangup()

Das Programm wird durch die Wahl der Nummer 351 aufgerufen. Dazu wird folgende Wahlregel in die extensions.conf von Asterisk eingefügt:

exten => 351,1,Noop(Wetter in Norrköping: "${CALLERID(name)}" <${CALLERID(num)}>)
    same => n,Answer()
    same => n,AGI(temp-norrkoeping.py)
    same => n,Hangup()

Das Skript lässt sich nach demselben Schema noch erweitern, in dem die Temperaturen der nächsten Tage aufgerufen werden können.

Weiterführend:

Nützliche Icons für Python auf meinem Desktop
Wie ich mir Python beibringe und warum es mir so großen Spaß macht – 13.2.2023 (aktualisiert am 13.3.2023): Hier verrate ich, wie ich meine Lernumgebung eingerichtet habe, welche Programme ich verwende, welches Manuskript ich lese und welches Vorlesungsvideo mir gefällt und welcher Lernstil mir am besten liegt. Alles ist kostenlos im Internet vorhanden.

Ganz nebenbei entsteht hier auch ein chronologisch geordnete Sammlung an Python-Skripten zum Üben. Sie dient mir auch als Gedankenstütze. – weiter

Mein Telefon-Server läuft wartungsfrei rund um die Uhr auf einem Raspberry Pi 3 B+. Die Telefon-Teilnehmer sind weltweit verteilt. Jeder kann mitmachen.
Hurra! Mein Asterisk-Telefon-Server-Tutorial ist fertiggestellt – 9. März 2023: Das Tutorial besteht aus genau 40 Abschnitten und es ermöglicht Anfängern, auf einem Raspberry Pi einen eigenen Telefon-Server zu betreiben und diesen mit anderen Asterisk-Servern zu vernetzen. Zusätzlich sind zahlreiche wirkungsvolle Schutzmaßnahmen wie APIBAN und Fail2Ban beschrieben, um einen sicheren Betrieb zu gewährleisten. Asterisk läuft bei mir dank APIBAN jetzt wartungsfrei rund um die Uhr. Das Tutorial beinhaltet auch die Programmierung von AGI-Skripten auf Python und anderen Programmiersprachen, wodurch es fast keine Grenzen bei der Umsetzung gibt. – weiter