Compare commits

...

5 Commits

Author SHA1 Message Date
admin f4fa3df73a Version 1.1.1: Regen via /weather/rain-daily, days relativ zu heute
- Regenberechnung nutzt neuen API-Endpunkt statt kumulativem Rohzähler
- days-Parameter relativ zu heute: funktioniert korrekt auch bei
  Aufruf mitten im Monat (z.B. 5.6. → zeigt trotzdem Mai komplett)
- Erster Tag des Folgemonats weiterhin im Chart sichtbar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:54:24 +02:00
admin db8feb317e Erster Tag des Folgemonats wird im Chart mit angezeigt
Ermöglicht korrekte Regenberechnung für den letzten Monatstag:
der Abschlusswert des kumulativen Zählers liegt oft erst im
Mitternachtswert des 1. Folgemonats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:30:13 +02:00
admin 91f4a7e4d2 Fix: Regen letzter Monatstag fehlt (kumulativer Zähler)
Query um +1 Tag erweitert, damit der erste Wert des Folgetags als
Abschlusswert für den letzten Tag verfügbar ist. Ohne diesen Wert
liefert max-min = 0 wenn der Zähler nur einmal pro Tag aktualisiert wird.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:22:00 +02:00
admin 37127fada3 Fix: 31. des Monats fehlt – API-End auf Monatsersten (exklusiv) gesetzt
end = first_of_this (Juni 1 00:00) statt May 31 23:59:59, damit APIs
mit exklusivem End-Parameter alle Tage des letzten Monats liefern.
Zusätzlich ±12h Padding auf xlim der Linien-Charts (wie Balkendiagramm).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 20:59:50 +02:00
admin 396587554a Fix: Version aus APP_VERSION-Env statt pyproject.toml lesen
pyproject.toml wird nicht ins Docker-Image kopiert und war zur Laufzeit
nicht vorhanden. deploy.sh übergibt VERSION bereits als Build-Arg,
das jetzt als ENV APP_VERSION im Image gesetzt wird.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:46:49 +02:00
3 changed files with 22 additions and 27 deletions
+3
View File
@@ -5,6 +5,9 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
ARG VERSION=unknown
ENV APP_VERSION=${VERSION}
COPY weather_report.py . COPY weather_report.py .
RUN mkdir -p images RUN mkdir -p images
+1 -1
View File
@@ -1,5 +1,5 @@
[project] [project]
name = "weather2oag" name = "weather2oag"
version = "1.1.0" version = "1.1.1"
description = "Monatlicher Wetterbericht der Sternwarte Welzheim" description = "Monatlicher Wetterbericht der Sternwarte Welzheim"
requires-python = ">=3.12" requires-python = ">=3.12"
+18 -26
View File
@@ -2,11 +2,9 @@
"""Erstellt einen kombinierten Wetterbericht für den letzten Kalendermonat.""" """Erstellt einen kombinierten Wetterbericht für den letzten Kalendermonat."""
import os import os
import pathlib
import re
import smtplib import smtplib
import tempfile import tempfile
from collections import defaultdict
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
@@ -20,9 +18,7 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
_v = re.search(r'^version\s*=\s*"([^"]+)"', VERSION = os.getenv("APP_VERSION", "?")
pathlib.Path("pyproject.toml").read_text(), re.MULTILINE)
VERSION = _v.group(1) if _v else "?"
API_BASE_URL = os.getenv("API_BASE_URL", "https://stwwetter.fuerst-stuttgart.de/api") API_BASE_URL = os.getenv("API_BASE_URL", "https://stwwetter.fuerst-stuttgart.de/api")
MAIL_SERVER = os.getenv("MAIL_SERVER") MAIL_SERVER = os.getenv("MAIL_SERVER")
@@ -52,8 +48,8 @@ plt.rcParams.update({
def last_month_range() -> tuple[datetime, datetime, str]: def last_month_range() -> tuple[datetime, datetime, str]:
today = datetime.now(timezone.utc) today = datetime.now(timezone.utc)
first_of_this = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0) first_of_this = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = first_of_this - timedelta(seconds=1) start = (first_of_this - timedelta(days=1)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
start = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) end = first_of_this + timedelta(days=1) # inkl. 1. Folgemonat
label = f"{MONTHS_DE[start.month - 1]} {start.year}" label = f"{MONTHS_DE[start.month - 1]} {start.year}"
return start, end, label return start, end, label
@@ -89,23 +85,19 @@ def _data_temp_hourly(start: datetime, end: datetime):
def _data_rain_daily(start: datetime, end: datetime): def _data_rain_daily(start: datetime, end: datetime):
data = fetch("/weather/range", { today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
"start": start.isoformat(), "end": end.isoformat(), "limit": 50000, days = min((today - start).days + 2, 365)
}) data = fetch("/weather/rain-daily", {"days": days})
by_day: dict[str, list[float]] = defaultdict(list) pairs = []
for d in data: for d in data:
if d.get("rain") is not None: day_dt = parse_dt(d["date"]).replace(hour=0, minute=0, second=0, microsecond=0)
by_day[parse_dt(d["datetime"]).strftime("%Y-%m-%d")].append(d["rain"]) if start <= day_dt < end:
pairs.append((day_dt, round(d["total_rain"], 1)))
dates, rain = [], [] pairs.sort()
for day_key in sorted(by_day): if not pairs:
vals = by_day[day_key] return [], []
daily = max(vals) - min(vals) dates, rain = zip(*pairs)
if daily < 0: return list(dates), list(rain)
daily = max(vals)
dates.append(datetime.fromisoformat(day_key).replace(tzinfo=timezone.utc))
rain.append(round(daily, 1))
return dates, rain
def create_combined_chart(start: datetime, end: datetime, label: str) -> bytes: def create_combined_chart(start: datetime, end: datetime, label: str) -> bytes:
@@ -125,7 +117,7 @@ def create_combined_chart(start: datetime, end: datetime, label: str) -> bytes:
ax.fill_between(dates_mm, t_min, t_max, alpha=0.12, color="#888888") ax.fill_between(dates_mm, t_min, t_max, alpha=0.12, color="#888888")
ax.set_title("Temperaturverlauf (Tages-Min / Tages-Max)", fontweight="bold", pad=10) ax.set_title("Temperaturverlauf (Tages-Min / Tages-Max)", fontweight="bold", pad=10)
ax.set_ylabel("Temperatur (°C)") ax.set_ylabel("Temperatur (°C)")
ax.set_xlim(start, end) ax.set_xlim(start - timedelta(hours=12), end + timedelta(hours=12))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m.")) ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m."))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=3)) ax.xaxis.set_major_locator(mdates.DayLocator(interval=3))
plt.setp(ax.get_xticklabels(), rotation=0, ha="center") plt.setp(ax.get_xticklabels(), rotation=0, ha="center")
@@ -136,7 +128,7 @@ def create_combined_chart(start: datetime, end: datetime, label: str) -> bytes:
ax.plot(dates_h, temps, color="#2a7be0", linewidth=1.5, label="Stundenmittel") ax.plot(dates_h, temps, color="#2a7be0", linewidth=1.5, label="Stundenmittel")
ax.set_title("Temperaturverlauf (Stundenmittel)", fontweight="bold", pad=10) ax.set_title("Temperaturverlauf (Stundenmittel)", fontweight="bold", pad=10)
ax.set_ylabel("Temperatur (°C)") ax.set_ylabel("Temperatur (°C)")
ax.set_xlim(start, end) ax.set_xlim(start - timedelta(hours=12), end + timedelta(hours=12))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m.")) ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m."))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=3)) ax.xaxis.set_major_locator(mdates.DayLocator(interval=3))
plt.setp(ax.get_xticklabels(), rotation=0, ha="center") plt.setp(ax.get_xticklabels(), rotation=0, ha="center")