Sprache auswählen

https://pixabay.com/de/users/clker-free-vector-images-3736/

Das wichtigste Gesprächsthema aller Menschen ist ja bekanntlich das Wetter. Daher lohnt es sich, einmal eine Wetter App zu schreiben, die uns zu diesem Thema auf dem Laufenden hält. 

Wetter API

Wer eine Wetter App schreiben möchte, muss zuerst einmal wissen, woher er die Daten zum aktuellen und zukünftigen Wetter bekommt. Eine bekannte und recht zuverlässige Quelle ist hier die API von OpenWeaterMap. Für private Anwendungen bekommt man hier aktuelle Wetterdaten, ebenso wie Vorhersagen kostenlos und aktuell zur Verfügung gestellt. Zunächst muss man sich registrieren. dadurch bekommt man eine APPID zugewiesen über die man sich bei Requests über die API authentifiziert.

Damit haben wir schon alles, was wir benötigen, um Wetterdaten abzurufen.

Aktuelles Wetter abrufen

Wir wollen uns zunächst einmal ansehen, was für Daten wir da überhaupt anfordern und wie wir das auswerten könnten. Dazu benutzen wir einfach unseren Browser und geben in die Adresszeile die Anfrage nach dem aktuellen Wetter einer beliebigen Stadt ein. Ich nehme jetzt mal das offizielle Beispiel auf der OpenWeatherMap Homepage.

Was passiert da im Detail. Zunächst einmal baut man sich einfach eine URL auf. Diese enthält zunächst einmal eine Spezifikation dazu, was man eigentlich will. Ich möchte das aktuelle Wetter. Damit ergibt sich der erste Teil der URL zu:

api.openweathermap.org/data/2.5/weather?

Anschließend muss man natürlich noch sagen, welche Stadt man angezeigt bekommen möchte. Dazu kann man den Städtenamen plus Länderkürzel direkt eingeben oder man verwendet eine eindeutige Nummer. Diese kann man sich hier raussuchen. Im Beispiel wird Cairns verwendet.

id=2172797

Anschließend kommt noch die APPID dazu (hier bitte euren eigenen eintragen) und schon bekommt man das aktuelle Wetter angezeigt.

&appid=b6907d289e10d714a6e88b30761fae22

Wir sehen, dass man als Standardantwort hier wohl eine JSON Dateien bekommt, in der jede Menge Information stecken. Angefangen bei den Koordinaten der angefragten Stadt, über das Wetter mit einer textuellen Beschreibung, einem Code für ein Bildchen, der Temperatur, Luftfeuchtigkeit usw. Alle Details und Beschreibungen zu den Werten findet man in der API Spezifikation.

Wettervorhersage anzeigen

Dann wollen wir mal sehen, was wir bekommen, wenn wir die Wettervorhersage abrufen. Auch dazu kann man sich auf der Homepage ein einfaches Beispiel anzeigen lassen. Im Detail bekommt man eine 5 Tagesvorhersage, die uns in drei Stunden Schritten das Wetter vorhersagt.

Der API call unterscheidet sich hier nur in Details von dem für das aktuelle Wetter. Statt weather? wird einfach 

forecast?

eingesetzt. Auch hier bekommen wir eine JSON Datei zurück.

Die GUI

Unsere GUI wird relativ einfach, zeigt aber alle Daten an, die man so braucht. Das Layout erstellen wir anhand der folgenden Skizze mit den gezeigten Größenverhältnissen.

in der oberen Hälft wird angezeigt, welche Stadt wir eigentlich gerade ansehen und was für ein aktuelles Wetter vorherrscht. In der untern Hälfte kommt dann die Vorhersage.

Das Layout übersetzen wir jetzt erst einmal in eine kv Datei. Die Datenverarbeitung im Pyhton file folgt dann im Anschluss.

<WeatherTestLayout>:
    orientation: 'vertical'
    on_touch_down:
        self.ids.wdgt1.download_current_weather(city = 'Nassau,bs')
        self.ids.wdgt1.download_forecast(city  = 'Nassau,bs')
    WeatherWidget:
        id: wdgt1
        
<WeatherWidget>:
    Label:
        id: lbl1
        text: root.city + ' ' + root.country
        font_size: 50
        size_hint: 1, .25
        pos_hint: {'top':1, 'left':1}
    AsyncImage:
        id: img1
        source: root.image_source
        allow_stretch: True
        keep_ratio: True
        size_hint: .25, .25
        pos_hint: {'top':.75, 'left':1}
    Label:
        id: lbl2
        text: root.temperature + u' \xb0C'
        font_size: 50
        size_hint: .25, .25
        pos_hint: {'top':.75, 'x':.25}
    Label:
        id: lbl3
        text: root.humidity + ' %rH'
        font_size: 25
        size_hint: .25, .125
        pos_hint: {'top':.75, 'x':.5}
    Label:
        id: lbl4
        text: root.pressure + ' hPa'
        font_size: 25
        size_hint: .25, .125
        pos_hint: {'top':.75, 'x':.75}
    Label:
        id: lbl5
        text: root.wind_speed + ' m/s'
        font_size: 25
        size_hint: .25, .125
        pos_hint: {'top':.625, 'x':.5}
    Label:
        id: lbl6
        text: root.wind_direction + u' \xb0'
        font_size: 25
        size_hint: .25, .125
        pos_hint: {'top':.625, 'x':.75}
    ScrollView:
        size_hint: 1, .5
        pos_hint: {'top': .5, 'left': 1}
        id: scroll
        GridLayout:
            id: grid
            size_hint: 1, None
            height: self.minimum_height
            cols: 3
            row_force_default: True
            row_default_height: 40

Wie im Beispiel des einfachen Widgets zur Anzeige von Temperatur und Feucht, werden auch hier die Anzeigewerte per Binding an Variablen im Pyhton file gebunden und damit auf einfache Weise aktualisiert, sobald sich in diesem etwas ändert. Jetzt können wir unser Pyhton file erstellen.

Die App verwendet hier asynchrone HTTP Requests um sicherzustellen, dass die App auch während des Updatens weiterhin auf Benutzereingaben reagiert und nicht einfriert. Dazu verwende ich die Standard Kivy Funktionen UrlRequest. Die Syntax ist schnell erklärt. Man übergibt die gewünscht URL und definiert, was passieren sollen, wenn dier Request erfolgreich (on_success) oder fehlerhaft (on_error), abgeschlossen wird. In diesen Fällen werden entweder die Funktionen zu weiterverarbeiten der erhaltenen Daten aufgerufen oder in meinem Fall Fehlertexte. Da die Daten, welche ich von OpenWeatherMap zurückbekomme im JSON Format sind, kann ich auch noch den Vorteil nutzen, dass die UrlRequest Funktion JSON automaitsch dekodiert und ich keine weiteren Umwandlungen machen muss.

Details zur UrlRequest Funktion von Kivy gibt es hier.

# encoding: utf-8

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty, ObjectProperty, StringProperty
from kivy.uix.image import AsyncImage
from kivy.uix.label import Label
import urllib.request
import json
from time import gmtime, strftime
from kivy.network.urlrequest import UrlRequest

class WeatherWidget(RelativeLayout):
    '''Shows current weather and 5 day forecast based on OpenWeatherMap API.
    Attributes:
    The attributes are bound by name to propertis in the kv file. Updating them will automatically update the displayed data in the visualisation
    
    city (StringProperty, str):
        Name of the city.
        Initially set to --.
    country (StringProperty, str):
        Country code according to ISO where the city is located in.
        Initially set to --.
    temperature (StringProperty, str):
        Current temperature.
        Initially set to --.
    humidity (StringProperty, str):
        Current humidity.
        Initially set to --.
    pressure (StringProperty, str):
        Current humidity.
        Initially set to --.
    wind_speed (StringProperty, str):
        Current wind speed.
        Initially set to --.
    wind_direction (StringProperty, str):
        Current wind direction.
        Initially set to --.
    last_update (StringProperty, str):
        Time at which the data has been updated last from OpenWeatherMap.
        Initially set to --.
    notification  (StringProperty, str):
        Error string. Shows exceptions, like no data available.
        Initially set to --.
    image_source (StringProperty, str):
        url of the icon showing the current weather conditions.
        Initially set to --.
'''

    city = StringProperty('--')
    country = StringProperty('--')
    temperature = StringProperty('--')
    humidity = StringProperty('--')
    pressure = StringProperty('--')
    image = StringProperty('--')
    wind_speed = StringProperty('--')
    wind_direction = StringProperty('--')
    last_update = StringProperty('--')
    notification = StringProperty('')
    image_source = StringProperty('')
    forecast = []
    
    def download_current_weather(self, city, *args, **kwargs):
        url = 'http://api.openweathermap.org/data/2.5/weather?q=' + city + '&APPID=YourAPPIDGoesHere'
        UrlRequest(url = url, on_success = self.show_current_weather, on_error = self.download_error, on_progress = self.progress, chunk_size = 40960)

    def download_forecast(self, city, *args, **kwargs):
        url = 'http://api.openweathermap.org/data/2.5/forecast?q=' + city + '&APPID=YourAPPIDGoesHere' 
        UrlRequest(url = url, on_success = self.show_forecast, on_error = self.download_error, on_progress = self.progress, chunk_size = 40960)

    def download_error(self, request, error):
        '''notify on error'''
        self.notification = 'data could not be downloaded'

    def progress(self, request, current_size, total_size):
        '''show progress to the user'''
        self.notification = ('Downloading data: {} bytes of {} bytes'.format(current_size, total_size))

    def show_current_weather(self, request, result):
        '''update displayed data for current weather.
        The data is stored as described in the OpenWeatherMap API.
        https://openweathermap.org/api/one-call-api?gclid=EAIaIQobChMI1bGR2-Dd6AIVxuN3Ch1sWAgGEAAYAiAAEgK6E_D_BwE
        The keys are read using the dictionary get() method because OpenWeatherMap does not always provide all data.
        Keys which are not available will be read as 'nn'
        Args:
            *args (): not used. For further development.
            **kwargs (): not used. For further development.
        Returns:
            (float): Scaled value.
        Raises:
            FileNotFoundError: Raised if no now.json file is found
        The files searched for in the folder where teh MyVisuApp.py file is located. See get_http_to_json for reference
        '''

        self.city = result.get('name','nn')
        self.country = result.get('sys').get('country','nn')
        self.temperature = '{:.0f}'.format(result.get('main').get('temp') - 273.15)
        self.humidity = str(result.get('main').get('humidity','nn'))
        self.pressure = str(result.get('main').get('pressure','nn'))
        self.image_source = 'http://openweathermap.org/img/w/' + result['weather'][0]['icon'] + '.png'
        self.wind_speed = str(result.get('wind').get('speed','nn'))
        self.wind_direction = str(result.get('wind').get('deg','nn'))
        self.last_update = str(gmtime(result.get('dt')))

        self.notification = ''

    
    def show_forecast(self, request, result):
        '''update displayed data for 5 day forecast.
        The data is stored as described in the OpenWeatherMap API.
        https://openweathermap.org/api/one-call-api?gclid=EAIaIQobChMI1bGR2-Dd6AIVxuN3Ch1sWAgGEAAYAiAAEgK6E_D_BwE
        Args:
            *args (): not used. For further development.
            **kwargs (): not used. For further development.
        Returns:
            (float): Scaled value.
        Raises:
            FileNotFoundError: Raised if forecast.json cannot be found
        '''
                
        self.forecast = result

        #first remove previous forecast 
        self.ids['grid'].clear_widgets()
        
        #add latest info
        for count in self.forecast['list']:

            #the time is formatted using strftime to display only the days name and hh:mm 
            self.ids['grid'].add_widget(Label(text=strftime('%a %H:%M',gmtime(count['dt'])), font_size='30sp'))
            #temperature will be rounded to two decimals
            self.ids['grid'].add_widget(Label(text='{:.0f}'.format(count['main']['temp'] - 273.15) + '°C', font_size='30sp'))
            self.ids['grid'].add_widget(AsyncImage(source='http://openweathermap.org/img/w/' + count['weather'][0]['icon'] + '.png'))

        self.notification = ''

class WeatherTestLayout(BoxLayout):
    pass

class WeatherWidgetApp(App):
    def build(self):
        return WeatherTestLayout()

if __name__ == '__main__':
    WeatherWidgetApp().run()

Das Ergebnis

Und damit haben wir eine einfache, aber detaillierte Wetter App geschrieben. Viel Spaß beim Anpassen und verbessern.

 Quellcode auf GitHub

Der aktuellste Code ist immer auf GitHub verfügbar