diff --git a/collector/main.py b/collector/main.py index be2bf17..f249723 100644 --- a/collector/main.py +++ b/collector/main.py @@ -38,84 +38,116 @@ app = FastAPI(title="Weather Data Collector API") # Pydantic Models class WeatherDataInput(BaseModel): - # Unterstütze beide Formate: datetime (String) oder dateTime (Unix-Timestamp) + # Zeitstempel: ISO-String (time), datetime-String oder Unix-Timestamp + time: str | None = None datetime: str | None = None dateTime: int | None = None - - # Unterstütze beide Feldnamen + + # Außentemperatur (Celsius): tempOut, temperature oder outTemp (Fahrenheit) + tempOut: float | None = None # Celsius (neues Format) temperature: float | None = None - outTemp: float | None = None # Fahrenheit - + outTemp: float | None = None # Fahrenheit (altes Format) + + # Innentemperatur + tempIn: float | None = None # Celsius + + # Außenfeuchte + humOut: int | None = None humidity: int | None = None outHumidity: float | None = None - + + # Innenfeuchte + humIn: int | None = None + + # Luftdruck pressure: float | None = None barometer: float | None = None # inHg - - windSpeed: float | None = None # mph + barTrend: int | None = None # hPa/Stunde + + # Wind + windAvg: float | None = None # m/s Durchschnitt (neues Format) + windSpeed: float | None = None wind_speed: float | None = None - - windGust: float | None = None # mph + windGust: float | None = None wind_gust: float | None = None - windDir: float | None = None wind_dir: float | None = None - + + # Niederschlag rain: float | None = None rainRate: float | None = None rain_rate: float | None = None - + + # Vorhersage + forecast: int | None = None + model_config = {"extra": "allow"} - + def get_datetime_string(self) -> str: - """Konvertiere dateTime (Unix-Timestamp) zu datetime (String)""" - if self.datetime: + """Zeitstempel als String zurückgeben""" + if self.time: + return self.time + elif self.datetime: return self.datetime elif self.dateTime: from datetime import datetime as dt return dt.fromtimestamp(self.dateTime).strftime('%Y-%m-%d %H:%M:%S') - raise ValueError("Weder datetime noch dateTime vorhanden") - + raise ValueError("Kein Zeitstempel vorhanden (time, datetime oder dateTime)") + def get_temperature_celsius(self) -> float | None: - """Konvertiere Temperatur von Fahrenheit zu Celsius falls nötig""" - if self.temperature is not None: + """Außentemperatur in Celsius""" + if self.tempOut is not None: + return self.tempOut + elif self.temperature is not None: return self.temperature elif self.outTemp is not None: - # Fahrenheit zu Celsius: (F - 32) * 5/9 return (self.outTemp - 32) * 5 / 9 return None - + + def get_temp_in(self) -> float | None: + """Innentemperatur in Celsius""" + return self.tempIn + def get_humidity_int(self) -> int | None: - """Hole Humidity-Wert""" - if self.humidity is not None: + """Außenfeuchte""" + if self.humOut is not None: + return int(self.humOut) + elif self.humidity is not None: return int(self.humidity) elif self.outHumidity is not None: return int(self.outHumidity) return None - + + def get_humidity_in(self) -> int | None: + """Innenfeuchte""" + return int(self.humIn) if self.humIn is not None else None + def get_pressure_hpa(self) -> float | None: - """Konvertiere Druck von inHg zu hPa falls nötig""" + """Luftdruck in hPa""" if self.pressure is not None: return self.pressure elif self.barometer is not None: - # inHg zu hPa: inHg * 33.8639 return self.barometer * 33.8639 return None - + def get_wind_speed(self) -> float | None: - """Hole Windgeschwindigkeit""" - return self.windSpeed if self.windSpeed is not None else self.wind_speed - + """Durchschnittliche Windgeschwindigkeit""" + if self.windAvg is not None: + return self.windAvg + elif self.windSpeed is not None: + return self.windSpeed + return self.wind_speed + def get_wind_gust(self) -> float | None: - """Hole Windböen""" + """Windböe""" return self.windGust if self.windGust is not None else self.wind_gust - + def get_wind_dir(self) -> float | None: - """Hole Windrichtung""" + """Windrichtung""" return self.windDir if self.windDir is not None else self.wind_dir - + def get_rain_rate(self) -> float | None: - """Hole Regenrate""" + """Regenrate""" return self.rainRate if self.rainRate is not None else self.rain_rate @@ -137,7 +169,7 @@ def get_db_connection(): def setup_database(): - """Tabelle erstellen falls nicht vorhanden""" + """Tabelle erstellen und fehlende Spalten ergänzen""" try: conn = get_db_connection() with conn.cursor() as cursor: @@ -157,8 +189,13 @@ def setup_database(): UNIQUE(datetime) ) """) + # Neue Spalten ergänzen (idempotent) + cursor.execute("ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS temp_in FLOAT") + cursor.execute("ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS humidity_in INTEGER") + cursor.execute("ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS forecast INTEGER") + cursor.execute("ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS bar_trend INTEGER") conn.commit() - logger.info("Tabelle weather_data bereit") + logger.info("Tabelle weather_data bereit (inkl. neuer Spalten)") conn.close() except Exception as e: logger.error(f"Fehler bei Datenbanksetup: {e}") @@ -238,41 +275,59 @@ async def receive_weather_data(data: WeatherDataInput): # Konvertiere zu den richtigen Werten dt_string = data.get_datetime_string() temp_c = data.get_temperature_celsius() + temp_in = data.get_temp_in() humidity = data.get_humidity_int() + humidity_in = data.get_humidity_in() pressure = data.get_pressure_hpa() + bar_trend = data.barTrend wind_speed = data.get_wind_speed() wind_gust = data.get_wind_gust() wind_dir = data.get_wind_dir() rain = data.rain rain_rate = data.get_rain_rate() - - logger.info(f"Konvertierte Daten - datetime: {dt_string}, temp: {temp_c}°C, humidity: {humidity}%, pressure: {pressure} hPa") - + forecast = data.forecast + + logger.info( + f"Konvertierte Daten - datetime: {dt_string}, " + f"tempOut: {temp_c}°C, tempIn: {temp_in}°C, " + f"humOut: {humidity}%, humIn: {humidity_in}%, " + f"pressure: {pressure} hPa, barTrend: {bar_trend}" + ) + with conn.cursor() as cursor: cursor.execute(""" - INSERT INTO weather_data - (datetime, temperature, humidity, pressure, wind_speed, - wind_gust, wind_dir, rain, rain_rate) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + INSERT INTO weather_data + (datetime, temperature, temp_in, humidity, humidity_in, + pressure, bar_trend, wind_speed, wind_gust, wind_dir, + rain, rain_rate, forecast) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (datetime) DO UPDATE SET temperature = EXCLUDED.temperature, + temp_in = EXCLUDED.temp_in, humidity = EXCLUDED.humidity, + humidity_in = EXCLUDED.humidity_in, pressure = EXCLUDED.pressure, + bar_trend = EXCLUDED.bar_trend, wind_speed = EXCLUDED.wind_speed, wind_gust = EXCLUDED.wind_gust, wind_dir = EXCLUDED.wind_dir, rain = EXCLUDED.rain, - rain_rate = EXCLUDED.rain_rate + rain_rate = EXCLUDED.rain_rate, + forecast = EXCLUDED.forecast """, ( dt_string, temp_c, + temp_in, humidity, + humidity_in, pressure, + bar_trend, wind_speed, wind_gust, wind_dir, rain, - rain_rate + rain_rate, + forecast )) conn.commit() logger.info(f"Daten gespeichert für {dt_string} (UTC)") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a7d301..f7bf8e2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -62,7 +62,7 @@ function App() { // Vordefinierte Zeitbereiche switch (timeRange) { case '24h': - weatherUrl = `${baseUrl}/weather/history?hours=24` + weatherUrl = `${baseUrl}/weather/history?hours=24&limit=5000` rainUrl = null break case '7d': @@ -93,7 +93,7 @@ function App() { // Immer die aktuellen 24h-Daten für "Aktuell"-Anzeige laden if (timeRange !== '24h') { - const currentUrl = `${baseUrl}/weather/history?hours=24` + const currentUrl = `${baseUrl}/weather/history?hours=24&limit=5000` const currentResponse = await fetch(currentUrl) if (currentResponse.ok) { const currentDataResult = await currentResponse.json() diff --git a/frontend/src/components/WeatherDashboard.jsx b/frontend/src/components/WeatherDashboard.jsx index 9e4c348..ea3f8a3 100644 --- a/frontend/src/components/WeatherDashboard.jsx +++ b/frontend/src/components/WeatherDashboard.jsx @@ -205,6 +205,19 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } }, [timeRange]) + // Hilfsfunktion: Dynamischen Y-Bereich berechnen. + // minHalfSpan: halbe Mindestspanne (z.B. 5 → Bereich mind. 10 Einheiten) + const calcYRange = (values, minHalfSpan) => { + if (values.length === 0) return { yMin: null, yMax: null } + const min = Math.min(...values) + const max = Math.max(...values) + if (max - min < minHalfSpan * 2) { + const center = (max + min) / 2 + return { yMin: center - minHalfSpan, yMax: center + minHalfSpan } + } + return { yMin: min, yMax: max } + } + // Gemeinsame Chart-Optionen (angepasst an Zeitraum) const getCommonOptions = () => { // Prüfe, ob es ein custom range ist @@ -418,18 +431,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } } - const min = Math.min(...temps) - const max = Math.max(...temps) - const range = max - min - - let yMin = min - let yMax = max - - if (range < 10) { - const center = (max + min) / 2 - yMin = center - 5 - yMax = center + 5 - } + const { yMin, yMax } = calcYRange(temps, 5) return { ...getCommonOptions(), @@ -464,13 +466,16 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' }, [sortedData, temperatureSuffix, timeRange]) // Luftfeuchtigkeit Chart - const humidityOptions = useMemo(() => ({ + const humidityOptions = useMemo(() => { + const humidities = sortedData.filter(item => item.humidity != null).map(item => item.humidity) + const { yMin, yMax } = calcYRange(humidities, 10) + return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null }, - min: 40, - max: 100 + min: yMin, + max: yMax }, series: [{ name: 'Feuchte', @@ -492,7 +497,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' valueSuffix: ' %' } }] - }), [sortedData, timeRange]) + } + }, [sortedData, timeRange]) // Luftdruck Chart const pressureOptions = useMemo(() => { @@ -509,18 +515,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } } - const min = Math.min(...pressures) - const max = Math.max(...pressures) - const range = max - min - - let yMin = min - let yMax = max - - if (range < 40) { - const center = (max + min) / 2 - yMin = center - 20 - yMax = center + 20 - } + const { yMin, yMax } = calcYRange(pressures, 20) return { ...getCommonOptions(), @@ -640,48 +635,35 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365) - - // Bei 365d und custom >= 365 Tage: nur Windgeschwindigkeit, keine Böen - const series = hideGusts - ? [{ - name: 'Windgeschwindigkeit', - data: sortedData - .filter(item => item.wind_speed != null) - .map(item => [new Date(item.datetime).getTime(), item.wind_speed]), - color: 'rgb(153, 102, 255)', - fillColor: 'rgba(153, 102, 255, 0.1)', - type: 'area', - connectNulls: false, - gapSize: 2 * 24 * 3600 * 1000, - gapUnit: 'value', - tooltip: { - valueDecimals: 1, - valueSuffix: ' km/h' - } - }] - : [{ - name: 'Windgeschwindigkeit', - data: sortedData - .filter(item => item.wind_speed != null) - .map(item => [new Date(item.datetime).getTime(), item.wind_speed]), - color: 'rgb(153, 102, 255)', - fillColor: 'rgba(153, 102, 255, 0.1)', - type: 'area', - connectNulls: false, - gapSize: 2 * 24 * 3600 * 1000, - gapUnit: 'value', - tooltip: { - valueDecimals: 1, - valueSuffix: ' km/h' - } - }, { + console.log("Gust: ", hideGusts) + const windSpeedSeries = { + name: 'Windgeschwindigkeit', + data: sortedData + .filter(item => item.wind_speed != null) + .map(item => [new Date(item.datetime).getTime(), item.wind_speed]), + color: 'rgb(153, 102, 255)', + fillColor: 'rgba(153, 102, 255, 0.1)', + type: 'area', + connectNulls: false, + gapSize: 2 * 24 * 3600 * 1000, + gapUnit: 'value', + tooltip: { + valueDecimals: 1, + valueSuffix: ' km/h' + } + } + + const series = hideGusts + ? [windSpeedSeries] + : [windSpeedSeries, { name: 'Böe' + windGustSuffix, data: sortedData .filter(item => item.wind_gust != null) .map(item => [new Date(item.datetime).getTime(), item.wind_gust]), - color: 'rgb(255, 159, 64)', - fillColor: 'rgba(255, 159, 64, 0.1)', + color: 'rgb(255, 100, 0)', + fillColor: 'rgba(255, 100, 0, 0.15)', type: 'area', + lineWidth: 1.5, connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', @@ -690,9 +672,15 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' valueSuffix: ' km/h' } }] - return { ...getCommonOptions(), + legend: { + enabled: true, + align: 'right', + verticalAlign: 'top', + floating: true, + itemStyle: { fontSize: '11px', fontWeight: 'normal' } + }, plotOptions: { series: { marker: { @@ -706,7 +694,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' }, yAxis: { ...getCommonOptions().yAxis, - title: { text: null } + title: { text: null }, + min: 0 }, series } @@ -1058,7 +1047,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h
-

💨 Windspeed{aggregationSuffix}[km/h]

+

💨 Wind{aggregationSuffix}[km/h]

diff --git a/start-dev.sh b/start-dev.sh index cfd2826..077a053 100755 --- a/start-dev.sh +++ b/start-dev.sh @@ -21,6 +21,15 @@ echo "" # Kurz warten bis API bereit ist sleep 3 +# Collector starten +echo "📥 Starte Collector auf Port 8001..." +cd "$SCRIPT_DIR" +source .venv/bin/activate +python -m uvicorn collector.main:app --host 0.0.0.0 --port 8001 --reload & +COLLECTOR_PID=$! +echo "Collector gestartet mit PID $COLLECTOR_PID" +echo "" + # Frontend starten echo "🎨 Starte Frontend auf Port 3000..." cd "$SCRIPT_DIR/frontend" @@ -33,13 +42,14 @@ echo "✅ Alle Services gestartet!" echo "" echo "📊 API: http://localhost:8000" echo "📊 API Docs: http://localhost:8000/docs" +echo "📥 Collector: http://localhost:8001" echo "🌐 Frontend: http://localhost:3000" echo "" echo "Drücken Sie Ctrl+C um alle Services zu stoppen..." echo "" # Trap zum Beenden aller Prozesse -trap "echo ''; echo '🛑 Stoppe Services...'; kill $API_PID $FRONTEND_PID 2>/dev/null; exit 0" INT TERM +trap "echo ''; echo '🛑 Stoppe Services...'; kill $API_PID $COLLECTOR_PID $FRONTEND_PID 2>/dev/null; exit 0" INT TERM # Warte auf Beendigung wait