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
- Gehe zu: https://home.openweathermap.org/api_keys
- Registriere dich
- Erstelle einen API‑Key
- Trage ihn in
weather_config.pyein
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.statedarauf 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_iconsgespeichert. - 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

