Wetter‑App mit Kivy – Das vollständige Schritt‑für‑Schritt‑Tutorial

Eine Wetter‑App ist ein ideales Projekt, um API‑Anbindung, Datenverarbeitung, UI‑Design und Diagrammerstellung zu kombinieren. In diesem Tutorial entwickeln wir eine vollständige Wetter‑App mit Python und Kivy, die aktuelle Wetterdaten, eine 5‑Tage‑Vorhersage und interaktive Diagramme anzeigt.

1. Warum OpenWeatherMap?

OpenWeatherMap ist einer der bekanntesten Anbieter für Wetterdaten. Gründe:

  • kostenloser API‑Key
  • weltweite Abdeckung
  • einheitliches JSON‑Format
  • stabile Infrastruktur
  • viele Zusatzdaten (Wind, Regen, Bewölkung, Luftdruck, Icons, Prognosen)
  • einfache URLs
  • gute Dokumentation

Wir benötigen:

  • /weather → aktuelles Wetter
  • /forecast → 5‑Tage‑Vorhersage (3‑Stunden‑Intervalle)
  • /img/wn/ → Icons

Screenshot‑Tipp:
Zeige hier das OpenWeather‑Dashboard mit dem API‑Key‑Bereich.

2. Projektstruktur

Wir arbeiten mit einer klaren, modularen Struktur:

wetter_app/
│
├── app.py
│
├── screens/
│   ├── forecast_screen.py
│   └── widgets.py
│
├── services/
│   ├── weather_reader.py
│   ├── weather_icons.py
│   └── state.py
│
├── ui/
│   └── visualizer.kv
│
├── assets/
│   ├── weather_icons/
│   └── forecast_plot.png
│
└── config/
    └── weather_config.py

Diese Struktur trennt:

  • UI (KV‑Datei)
  • Logik (Services)
  • Screens (UI‑Logik)
  • Konfiguration
  • Assets

3. OpenWeather API vorbereiten

  1. Gehe zu: https://home.openweathermap.org/api_keys
  2. Registriere dich
  3. Erstelle einen API‑Key
  4. Trage ihn in weather_config.py ein

4. Vollständiger Code aller Dateien – mit Erklärungen

Jetzt kommt der wichtigste Teil:
Jede Datei vollständig, mit Erklärung, Zweck und Besonderheiten.

4.1 config/weather_config.py

Was macht diese Datei?

  • Sie enthält alle Konfigurationsparameter, die Nutzer später ändern können.
  • Sie trennt Konfiguration von Logik.
  • Sie macht die App wartbar und erweiterbar.

Besonderheiten

  • API‑Key wird hier zentral verwaltet.
  • Stadt, Sprache und Einheiten können später dynamisch erweitert werden.
API_KEY = "DEIN_API_KEY_HIER"
CITY = "Weinstadt"
LANG = "de"
UNITS = "metric"

4.2 services/state.py

Was macht diese Datei?

  • Sie speichert den globalen Zustand der App.
  • Screens greifen über app.state darauf zu.
  • Ideal für spätere Erweiterungen (z. B. Theme, Favoriten, Standortwahl).
class AppState:
    def __init__(self):
        self.city = "Weinstadt"

4.3 services/weather_icons.py

Was macht diese Datei?

  • Sie lädt Icons von OpenWeatherMap herunter.
  • Sie speichert sie lokal, um API‑Aufrufe zu reduzieren.
  • Sie sorgt für schnelle Ladezeiten und Offline‑Fähigkeit.

Besonderheiten

  • Icons werden im Ordner assets/weather_icons gespeichert.
  • Icons werden nur einmal heruntergeladen (Caching).
import requests
from pathlib import Path

ICON_DIR = Path("assets/weather_icons")
ICON_DIR.mkdir(parents=True, exist_ok=True)

def download_icon(icon_code: str) -> Path:
    path = ICON_DIR / f"{icon_code}.png"

    if path.exists():
        return path

    url = f"https://openweathermap.org/img/wn/{icon_code}@4x.png"
    response = requests.get(url)

    if response.status_code == 200:
        path.write_bytes(response.content)

    return path

4.4 services/weather_reader.py

Was macht diese Datei?

  • Sie kapselt alle API‑Zugriffe.
  • Sie liefert Python‑Dictionaries, die direkt im UI verwendet werden können.
  • Sie enthält robustes Fehlerhandling.

Besonderheiten

  • Konsistentes Datenmodell für aktuelle Daten und Vorhersage.
  • Regen und Schnee werden korrekt extrahiert.
  • Windrichtung wird berücksichtigt.
import requests
from config.weather_config import API_KEY, CITY, LANG, UNITS


def fetch_current_weather():
    """
    Ruft das aktuelle Wetter ab.
    Liefert alle wichtigen Werte:
    Temperatur, gefühlte Temperatur, Luftfeuchtigkeit, Druck,
    Windgeschwindigkeit, Windrichtung, Bewölkung, Beschreibung.
    """
    url = (
        "https://api.openweathermap.org/data/2.5/weather"
        f"?q={CITY}&appid={API_KEY}&units={UNITS}&lang={LANG}"
    )
    data = requests.get(url).json()

    # Fehlerfall (z. B. ungültiger API-Key)
    if "main" not in data:
        return {
            "temperature": 0,
            "feels_like": 0,
            "humidity": 0,
            "pressure": 0,
            "wind_speed": 0,
            "wind_deg": 0,
            "clouds": 0,
            "description": data.get("message", "Keine Daten"),
        }

    return {
        "temperature": data["main"]["temp"],
        "feels_like": data["main"]["feels_like"],
        "humidity": data["main"]["humidity"],
        "pressure": data["main"]["pressure"],
        "wind_speed": data["wind"]["speed"],
        "wind_deg": data["wind"].get("deg", 0),
        "clouds": data["clouds"]["all"],
        "description": data["weather"][0]["description"],
    }


def fetch_forecast():
    """
    Ruft die 5-Tage-Vorhersage ab (3h-Intervalle).
    Liefert pro Eintrag:
    Temperatur, gefühlte Temperatur, Luftfeuchtigkeit, Druck,
    Windgeschwindigkeit, Windrichtung, Regen, Schnee, Bewölkung, Icon.
    """
    url = (
        "https://api.openweathermap.org/data/2.5/forecast"
        f"?q={CITY}&appid={API_KEY}&units={UNITS}&lang={LANG}"
    )
    data = requests.get(url).json()

    if "list" not in data:
        return []

    forecast = []
    for entry in data["list"]:
        rain = entry.get("rain", {}).get("3h", 0)
        snow = entry.get("snow", {}).get("3h", 0)

        forecast.append({
            "timestamp": entry["dt_txt"],

            "temperature": entry["main"]["temp"],
            "feels_like": entry["main"]["feels_like"],
            "humidity": entry["main"]["humidity"],
            "pressure": entry["main"]["pressure"],

            "wind_speed": entry["wind"]["speed"],
            "wind_deg": entry["wind"].get("deg", 0),

            "rain": rain,
            "snow": snow,

            "clouds": entry["clouds"]["all"],
            "icon": entry["weather"][0]["icon"],
        })

    return forecast

4.5 screens/widgets.py

Was macht diese Datei?

  • Sie definiert das DayWidget, eine einzelne Tageskachel.
  • Jede Kachel zeigt:
    • Datum
    • Min/Max Temperatur
    • Icon

Besonderheiten

  • Die Darstellung erfolgt in der KV‑Datei.
  • Das Widget ist bewusst minimal gehalten.
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty

class DayWidget(BoxLayout):
    date = StringProperty("")
    min_temp = StringProperty("")
    max_temp = StringProperty("")
    icon_source = StringProperty("")

4.6 screens/forecast_screen.py

Was macht diese Datei?

Dies ist der Hauptscreen der App.
Er ist verantwortlich für:

  • Laden der aktuellen Wetterdaten
  • Laden der 5‑Tage‑Vorhersage
  • Erstellen der Tageskacheln
  • Erzeugen der Diagramme
  • Auto‑Reload alle 10 Minuten
  • Dropdown‑Auswahl für Diagramme

Besonderheiten

  • Diagramme werden mit Matplotlib erzeugt.
  • Das Diagramm wird als PNG gespeichert und im UI angezeigt.
  • Die Tageskacheln werden dynamisch erzeugt.
from kivy.uix.screenmanager import Screen
from kivy.clock import Clock
from kivy.app import App
from services.weather_reader import fetch_current_weather, fetch_forecast
from services.weather_icons import download_icon
from screens.widgets import DayWidget
import matplotlib.pyplot as plt
from pathlib import Path
from collections import defaultdict


class ForecastScreen(Screen):

    reload_event = None

    def on_enter(self):
        """Wird aufgerufen, wenn der Screen sichtbar wird."""
        self.load_data()
        self.reload_event = Clock.schedule_interval(self.load_data, 600)

    def on_leave(self):
        """Auto-Reload stoppen, wenn der Screen verlassen wird."""
        if self.reload_event:
            self.reload_event.cancel()

    def load_data(self, *args):
        """Lädt aktuelle Daten + Vorhersage und aktualisiert das UI."""
        self.load_current_weather()
        forecast = fetch_forecast()
        self.forecast_data = forecast
        self.build_day_widgets(forecast)
        self.update_chart("Temperatur")

    def load_current_weather(self):
        """Aktuelle Wetterdaten in die UI schreiben."""
        current = fetch_current_weather()

        self.ids.temp_label_forecast.text = f"{current['temperature']:.1f} °C"
        self.ids.hum_label_forecast.text = f"{current['humidity']} %"
        self.ids.desc_label_forecast.text = current["description"]
        self.ids.wind_label.text = f"Wind: {current['wind_speed']} m/s"
        self.ids.clouds_label.text = f"Wolken: {current['clouds']} %"

    def build_day_widgets(self, forecast):
        """
        Erstellt die Tageskacheln.
        Aggregiert Min/Max Temperatur und wählt ein repräsentatives Icon.
        """
        days = defaultdict(list)
        for entry in forecast:
            day = entry["timestamp"].split(" ")[0]
            days[day].append(entry)

        self.ids.days_box.clear_widgets()

        for day, entries in list(days.items())[:5]:
            temps = [e["temperature"] for e in entries]
            min_temp = min(temps)
            max_temp = max(temps)

            # Icon wählen: Mittagswert bevorzugt
            icon = None
            for e in entries:
                if "12:00" in e["timestamp"]:
                    icon = e["icon"]
                    break
            if not icon:
                icon = entries[0]["icon"]

            icon_path = download_icon(icon)

            self.ids.days_box.add_widget(
                DayWidget(
                    date=day[5:],
                    min_temp=f"{min_temp:.1f} °C",
                    max_temp=f"{max_temp:.1f} °C",
                    icon_source=str(icon_path),
                )
            )

    def update_chart(self, selection):
        """Wird vom Dropdown ausgelöst und erzeugt das passende Diagramm."""
        if not hasattr(self, "forecast_data"):
            return
        self.plot_chart(self.forecast_data, selection)

    def plot_chart(self, forecast, mode):
        """
        Zentrales Diagramm-Modul.
        Erzeugt je nach Modus ein Linien- oder Balkendiagramm.
        """
        timestamps = [f["timestamp"][5:16] for f in forecast]

        if mode == "Temperatur":
            values = [f["temperature"] for f in forecast]
            color = "red"
            ylabel = "Temperatur (°C)"
            plot_type = "line"

        elif mode == "Wind":
            values = [f["wind_speed"] for f in forecast]
            color = "green"
            ylabel = "Wind (m/s)"
            plot_type = "line"

        elif mode == "Regen":
            values = [f["rain"] for f in forecast]
            color = "blue"
            ylabel = "Regen (mm)"
            plot_type = "bar"

        elif mode == "Bewölkung":
            values = [f["clouds"] for f in forecast]
            color = "gray"
            ylabel = "Bewölkung (%)"
            plot_type = "line"

        fig, ax = plt.subplots(figsize=(6, 3), dpi=100)

        if plot_type == "line":
            ax.plot(timestamps, values, color=color)
        else:
            ax.bar(timestamps, values, color=color, alpha=0.5)

        ax.set_ylabel(ylabel, color=color)
        ax.tick_params(axis="y", labelcolor=color)

        # X-Achse lesbar machen
        step = max(1, len(timestamps) // 6)
        ax.set_xticks(range(0, len(timestamps), step))
        ax.set_xticklabels(
            [timestamps[i] for i in range(0, len(timestamps), step)],
            rotation=45,
            ha="right",
            fontsize=7
        )

        fig.tight_layout()
        path = Path("assets/forecast_plot.png")
        fig.savefig(path)
        plt.close(fig)

        self.ids.plot_image_forecast.source = str(path)
        self.ids.plot_image_forecast.reload()

4.7 ui/visualizer.kv

Was macht diese Datei?

  • Sie definiert das gesamte UI.
  • Sie enthält:
    • Farbvariablen
    • Dark‑Mode‑Design
    • Layout für DayWidget
    • Layout für ForecastScreen

Besonderheiten

  • Farbvariablen machen das UI leicht anpassbar.
  • DayWidget hat abgerundete Ecken und Dark‑Mode‑Hintergrund.
  • Die Struktur ist klar und gut lesbar.
#:set BG_COLOR (0.388, 0.569, 0.659, 1)        # #6391A8
#:set BG_COLOR_DARK (0.30, 0.45, 0.52, 1)      # dunklere Variante

# -------------------------------------------------------------
# DayWidget – einzelne Tageskachel
# -------------------------------------------------------------
<DayWidget>:
    orientation: "vertical"
    size_hint_x: 0.2
    padding: 6
    spacing: 6

    canvas.before:
        Color:
            rgba: BG_COLOR_DARK
        RoundedRectangle:
            pos: self.pos
            size: self.size
            radius: [8]

    Image:
        source: root.icon_source
        allow_stretch: True
        keep_ratio: True
        size_hint_y: 0.55

    Label:
        text: root.date
        font_size: "14sp"
        color: 1, 1, 1, 1
        halign: "center"
        valign: "middle"
        text_size: self.size

    Label:
        text: root.min_temp + " / " + root.max_temp
        font_size: "14sp"
        color: 1, 1, 1, 1
        halign: "center"
        valign: "middle"
        text_size: self.size



# -------------------------------------------------------------
# ForecastScreen – Hauptscreen der Wetter-App
# -------------------------------------------------------------
<ForecastScreen>:
    name: "forecast"

    canvas.before:
        Color:
            rgba: BG_COLOR
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        orientation: "vertical"
        padding: 12
        spacing: 12

        # -----------------------------------------------------
        # Kopfzeile
        # -----------------------------------------------------
        BoxLayout:
            size_hint_y: 0.1
            spacing: 10

            Button:
                text: "< Zurück"
                size_hint_x: 0.2
                background_color: BG_COLOR_DARK
                color: 1, 1, 1, 1
                on_release: root.manager.current = "start"

            Label:
                text: "Wettervorhersage - " + app.state.city
                font_size: "20sp"
                color: 1, 1, 1, 1


        # -----------------------------------------------------
        # Temperatur + Luftfeuchtigkeit
        # -----------------------------------------------------
        BoxLayout:
            orientation: "horizontal"
            size_hint_y: 0.1
            spacing: 20

            Label:
                id: temp_label_forecast
                text: "-- °C"
                font_size: "18sp"
                color: 1, 1, 1, 1

            Label:
                id: hum_label_forecast
                text: "-- %"
                font_size: "18sp"
                color: 1, 1, 1, 1


        # -----------------------------------------------------
        # Wind + Bewölkung
        # -----------------------------------------------------
        BoxLayout:
            orientation: "horizontal"
            size_hint_y: 0.1
            spacing: 20

            Label:
                id: wind_label
                text: "Wind: -- m/s"
                font_size: "16sp"
                color: 1, 1, 1, 1

            Label:
                id: clouds_label
                text: "Wolken: -- %"
                font_size: "16sp"
                color: 1, 1, 1, 1


        # -----------------------------------------------------
        # Beschreibung
        # -----------------------------------------------------
        Label:
            id: desc_label_forecast
            text: "--"
            font_size: "18sp"
            size_hint_y: 0.1
            color: 1, 1, 1, 1


        # -----------------------------------------------------
        # Diagramm-Auswahl
        # -----------------------------------------------------
        Spinner:
            id: chart_selector
            text: "Temperatur"
            values: ["Temperatur", "Wind", "Regen", "Bewölkung"]
            size_hint_y: 0.08
            background_color: BG_COLOR_DARK
            color: 1, 1, 1, 1
            on_text: root.update_chart(self.text)


        # -----------------------------------------------------
        # Tageskacheln
        # -----------------------------------------------------
        BoxLayout:
            id: days_box
            orientation: "horizontal"
            size_hint_y: 0.25
            spacing: 10


        # -----------------------------------------------------
        # Diagramm
        # -----------------------------------------------------
        Image:
            id: plot_image_forecast
            source: ""
            allow_stretch: True
            keep_ratio: True
            size_hint_y: 0.45

4.8 app.py

Was macht diese Datei?

  • Sie ist der Einstiegspunkt der App.
  • Sie lädt die KV‑Datei.
  • Sie initialisiert den globalen Zustand.
  • Sie registriert den ForecastScreen.

Besonderheiten

  • Die App ist modular aufgebaut.
  • Neue Screens können leicht ergänzt werden.
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager
from kivy.lang import Builder
from services.state import AppState
from screens.forecast_screen import ForecastScreen
from screens.widgets import DayWidget

class RootWidget(ScreenManager):
    pass

class WeatherApp(App):
    def build(self):
        self.state = AppState()
        Builder.load_file("ui/visualizer.kv")
        root = RootWidget()
        root.add_widget(ForecastScreen(name="forecast"))
        return root

if __name__ == "__main__":
    WeatherApp().run()

Nächste Schritte

Wenn du lernen willst, wie man schlussendlich ein Modul aus der App macht, schau dir diesen Artikel an