Mapping der neu gesendeten Werte auf die in der DB (main.py)

Anzeige der letzten 24h richtig (App.jsx)
Y-Bereichsberechnung für alle 3 (THP) dynamisch,
Windbön mit angezeigt
This commit is contained in:
2026-04-24 14:31:06 +02:00
parent f271ff455f
commit 3652831bc3
4 changed files with 172 additions and 118 deletions

View File

@@ -38,84 +38,116 @@ app = FastAPI(title="Weather Data Collector API")
# Pydantic Models # Pydantic Models
class WeatherDataInput(BaseModel): 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: str | None = None
dateTime: int | 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 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 humidity: int | None = None
outHumidity: float | None = None outHumidity: float | None = None
# Innenfeuchte
humIn: int | None = None
# Luftdruck
pressure: float | None = None pressure: float | None = None
barometer: float | None = None # inHg barometer: float | None = None # inHg
barTrend: int | None = None # hPa/Stunde
windSpeed: float | None = None # mph # Wind
windAvg: float | None = None # m/s Durchschnitt (neues Format)
windSpeed: float | None = None
wind_speed: float | None = None wind_speed: float | None = None
windGust: float | None = None
windGust: float | None = None # mph
wind_gust: float | None = None wind_gust: float | None = None
windDir: float | None = None windDir: float | None = None
wind_dir: float | None = None wind_dir: float | None = None
# Niederschlag
rain: float | None = None rain: float | None = None
rainRate: float | None = None rainRate: float | None = None
rain_rate: float | None = None rain_rate: float | None = None
# Vorhersage
forecast: int | None = None
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
def get_datetime_string(self) -> str: def get_datetime_string(self) -> str:
"""Konvertiere dateTime (Unix-Timestamp) zu datetime (String)""" """Zeitstempel als String zurückgeben"""
if self.datetime: if self.time:
return self.time
elif self.datetime:
return self.datetime return self.datetime
elif self.dateTime: elif self.dateTime:
from datetime import datetime as dt from datetime import datetime as dt
return dt.fromtimestamp(self.dateTime).strftime('%Y-%m-%d %H:%M:%S') 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: def get_temperature_celsius(self) -> float | None:
"""Konvertiere Temperatur von Fahrenheit zu Celsius falls nötig""" """Außentemperatur in Celsius"""
if self.temperature is not None: if self.tempOut is not None:
return self.tempOut
elif self.temperature is not None:
return self.temperature return self.temperature
elif self.outTemp is not None: elif self.outTemp is not None:
# Fahrenheit zu Celsius: (F - 32) * 5/9
return (self.outTemp - 32) * 5 / 9 return (self.outTemp - 32) * 5 / 9
return None return None
def get_temp_in(self) -> float | None:
"""Innentemperatur in Celsius"""
return self.tempIn
def get_humidity_int(self) -> int | None: def get_humidity_int(self) -> int | None:
"""Hole Humidity-Wert""" """Außenfeuchte"""
if self.humidity is not None: if self.humOut is not None:
return int(self.humOut)
elif self.humidity is not None:
return int(self.humidity) return int(self.humidity)
elif self.outHumidity is not None: elif self.outHumidity is not None:
return int(self.outHumidity) return int(self.outHumidity)
return None 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: 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: if self.pressure is not None:
return self.pressure return self.pressure
elif self.barometer is not None: elif self.barometer is not None:
# inHg zu hPa: inHg * 33.8639
return self.barometer * 33.8639 return self.barometer * 33.8639
return None return None
def get_wind_speed(self) -> float | None: def get_wind_speed(self) -> float | None:
"""Hole Windgeschwindigkeit""" """Durchschnittliche Windgeschwindigkeit"""
return self.windSpeed if self.windSpeed is not None else self.wind_speed 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: 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 return self.windGust if self.windGust is not None else self.wind_gust
def get_wind_dir(self) -> float | None: def get_wind_dir(self) -> float | None:
"""Hole Windrichtung""" """Windrichtung"""
return self.windDir if self.windDir is not None else self.wind_dir return self.windDir if self.windDir is not None else self.wind_dir
def get_rain_rate(self) -> float | None: def get_rain_rate(self) -> float | None:
"""Hole Regenrate""" """Regenrate"""
return self.rainRate if self.rainRate is not None else self.rain_rate return self.rainRate if self.rainRate is not None else self.rain_rate
@@ -137,7 +169,7 @@ def get_db_connection():
def setup_database(): def setup_database():
"""Tabelle erstellen falls nicht vorhanden""" """Tabelle erstellen und fehlende Spalten ergänzen"""
try: try:
conn = get_db_connection() conn = get_db_connection()
with conn.cursor() as cursor: with conn.cursor() as cursor:
@@ -157,8 +189,13 @@ def setup_database():
UNIQUE(datetime) 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() conn.commit()
logger.info("Tabelle weather_data bereit") logger.info("Tabelle weather_data bereit (inkl. neuer Spalten)")
conn.close() conn.close()
except Exception as e: except Exception as e:
logger.error(f"Fehler bei Datenbanksetup: {e}") logger.error(f"Fehler bei Datenbanksetup: {e}")
@@ -238,41 +275,59 @@ async def receive_weather_data(data: WeatherDataInput):
# Konvertiere zu den richtigen Werten # Konvertiere zu den richtigen Werten
dt_string = data.get_datetime_string() dt_string = data.get_datetime_string()
temp_c = data.get_temperature_celsius() temp_c = data.get_temperature_celsius()
temp_in = data.get_temp_in()
humidity = data.get_humidity_int() humidity = data.get_humidity_int()
humidity_in = data.get_humidity_in()
pressure = data.get_pressure_hpa() pressure = data.get_pressure_hpa()
bar_trend = data.barTrend
wind_speed = data.get_wind_speed() wind_speed = data.get_wind_speed()
wind_gust = data.get_wind_gust() wind_gust = data.get_wind_gust()
wind_dir = data.get_wind_dir() wind_dir = data.get_wind_dir()
rain = data.rain rain = data.rain
rain_rate = data.get_rain_rate() rain_rate = data.get_rain_rate()
forecast = data.forecast
logger.info(f"Konvertierte Daten - datetime: {dt_string}, temp: {temp_c}°C, humidity: {humidity}%, pressure: {pressure} hPa") 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: with conn.cursor() as cursor:
cursor.execute(""" cursor.execute("""
INSERT INTO weather_data INSERT INTO weather_data
(datetime, temperature, humidity, pressure, wind_speed, (datetime, temperature, temp_in, humidity, humidity_in,
wind_gust, wind_dir, rain, rain_rate) pressure, bar_trend, wind_speed, wind_gust, wind_dir,
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) rain, rain_rate, forecast)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (datetime) DO UPDATE SET ON CONFLICT (datetime) DO UPDATE SET
temperature = EXCLUDED.temperature, temperature = EXCLUDED.temperature,
temp_in = EXCLUDED.temp_in,
humidity = EXCLUDED.humidity, humidity = EXCLUDED.humidity,
humidity_in = EXCLUDED.humidity_in,
pressure = EXCLUDED.pressure, pressure = EXCLUDED.pressure,
bar_trend = EXCLUDED.bar_trend,
wind_speed = EXCLUDED.wind_speed, wind_speed = EXCLUDED.wind_speed,
wind_gust = EXCLUDED.wind_gust, wind_gust = EXCLUDED.wind_gust,
wind_dir = EXCLUDED.wind_dir, wind_dir = EXCLUDED.wind_dir,
rain = EXCLUDED.rain, rain = EXCLUDED.rain,
rain_rate = EXCLUDED.rain_rate rain_rate = EXCLUDED.rain_rate,
forecast = EXCLUDED.forecast
""", ( """, (
dt_string, dt_string,
temp_c, temp_c,
temp_in,
humidity, humidity,
humidity_in,
pressure, pressure,
bar_trend,
wind_speed, wind_speed,
wind_gust, wind_gust,
wind_dir, wind_dir,
rain, rain,
rain_rate rain_rate,
forecast
)) ))
conn.commit() conn.commit()
logger.info(f"Daten gespeichert für {dt_string} (UTC)") logger.info(f"Daten gespeichert für {dt_string} (UTC)")

View File

@@ -62,7 +62,7 @@ function App() {
// Vordefinierte Zeitbereiche // Vordefinierte Zeitbereiche
switch (timeRange) { switch (timeRange) {
case '24h': case '24h':
weatherUrl = `${baseUrl}/weather/history?hours=24` weatherUrl = `${baseUrl}/weather/history?hours=24&limit=5000`
rainUrl = null rainUrl = null
break break
case '7d': case '7d':
@@ -93,7 +93,7 @@ function App() {
// Immer die aktuellen 24h-Daten für "Aktuell"-Anzeige laden // Immer die aktuellen 24h-Daten für "Aktuell"-Anzeige laden
if (timeRange !== '24h') { if (timeRange !== '24h') {
const currentUrl = `${baseUrl}/weather/history?hours=24` const currentUrl = `${baseUrl}/weather/history?hours=24&limit=5000`
const currentResponse = await fetch(currentUrl) const currentResponse = await fetch(currentUrl)
if (currentResponse.ok) { if (currentResponse.ok) {
const currentDataResult = await currentResponse.json() const currentDataResult = await currentResponse.json()

View File

@@ -205,6 +205,19 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
} }
}, [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) // Gemeinsame Chart-Optionen (angepasst an Zeitraum)
const getCommonOptions = () => { const getCommonOptions = () => {
// Prüfe, ob es ein custom range ist // Prüfe, ob es ein custom range ist
@@ -418,18 +431,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
} }
} }
const min = Math.min(...temps) const { yMin, yMax } = calcYRange(temps, 5)
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
}
return { return {
...getCommonOptions(), ...getCommonOptions(),
@@ -464,13 +466,16 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
}, [sortedData, temperatureSuffix, timeRange]) }, [sortedData, temperatureSuffix, timeRange])
// Luftfeuchtigkeit Chart // 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(), ...getCommonOptions(),
yAxis: { yAxis: {
...getCommonOptions().yAxis, ...getCommonOptions().yAxis,
title: { text: null }, title: { text: null },
min: 40, min: yMin,
max: 100 max: yMax
}, },
series: [{ series: [{
name: 'Feuchte', name: 'Feuchte',
@@ -492,7 +497,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
valueSuffix: ' %' valueSuffix: ' %'
} }
}] }]
}), [sortedData, timeRange]) }
}, [sortedData, timeRange])
// Luftdruck Chart // Luftdruck Chart
const pressureOptions = useMemo(() => { const pressureOptions = useMemo(() => {
@@ -509,18 +515,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
} }
} }
const min = Math.min(...pressures) const { yMin, yMax } = calcYRange(pressures, 20)
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
}
return { return {
...getCommonOptions(), ...getCommonOptions(),
@@ -640,48 +635,35 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
const customDays = isCustomRange ? (timeRange.days || 1) : 0 const customDays = isCustomRange ? (timeRange.days || 1) : 0
const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365) const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365)
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'
}
}
// Bei 365d und custom >= 365 Tage: nur Windgeschwindigkeit, keine Böen
const series = hideGusts const series = hideGusts
? [{ ? [windSpeedSeries]
name: 'Windgeschwindigkeit', : [windSpeedSeries, {
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'
}
}, {
name: 'Böe' + windGustSuffix, name: 'Böe' + windGustSuffix,
data: sortedData data: sortedData
.filter(item => item.wind_gust != null) .filter(item => item.wind_gust != null)
.map(item => [new Date(item.datetime).getTime(), item.wind_gust]), .map(item => [new Date(item.datetime).getTime(), item.wind_gust]),
color: 'rgb(255, 159, 64)', color: 'rgb(255, 100, 0)',
fillColor: 'rgba(255, 159, 64, 0.1)', fillColor: 'rgba(255, 100, 0, 0.15)',
type: 'area', type: 'area',
lineWidth: 1.5,
connectNulls: false, connectNulls: false,
gapSize: 2 * 24 * 3600 * 1000, gapSize: 2 * 24 * 3600 * 1000,
gapUnit: 'value', gapUnit: 'value',
@@ -690,9 +672,15 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
valueSuffix: ' km/h' valueSuffix: ' km/h'
} }
}] }]
return { return {
...getCommonOptions(), ...getCommonOptions(),
legend: {
enabled: true,
align: 'right',
verticalAlign: 'top',
floating: true,
itemStyle: { fontSize: '11px', fontWeight: 'normal' }
},
plotOptions: { plotOptions: {
series: { series: {
marker: { marker: {
@@ -706,7 +694,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
}, },
yAxis: { yAxis: {
...getCommonOptions().yAxis, ...getCommonOptions().yAxis,
title: { text: null } title: { text: null },
min: 0
}, },
series series
} }
@@ -1058,7 +1047,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
<div className="chart-item"> <div className="chart-item">
<div className="current-value">Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</div> <div className="current-value">Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</div>
<div className="chart-container"> <div className="chart-container">
<h3><span>💨 Windspeed{aggregationSuffix}</span><span className="unit">[km/h]</span></h3> <h3><span>💨 Wind{aggregationSuffix}</span><span className="unit">[km/h]</span></h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} /> <HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div> </div>

View File

@@ -21,6 +21,15 @@ echo ""
# Kurz warten bis API bereit ist # Kurz warten bis API bereit ist
sleep 3 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 # Frontend starten
echo "🎨 Starte Frontend auf Port 3000..." echo "🎨 Starte Frontend auf Port 3000..."
cd "$SCRIPT_DIR/frontend" cd "$SCRIPT_DIR/frontend"
@@ -33,13 +42,14 @@ echo "✅ Alle Services gestartet!"
echo "" echo ""
echo "📊 API: http://localhost:8000" echo "📊 API: http://localhost:8000"
echo "📊 API Docs: http://localhost:8000/docs" echo "📊 API Docs: http://localhost:8000/docs"
echo "📥 Collector: http://localhost:8001"
echo "🌐 Frontend: http://localhost:3000" echo "🌐 Frontend: http://localhost:3000"
echo "" echo ""
echo "Drücken Sie Ctrl+C um alle Services zu stoppen..." echo "Drücken Sie Ctrl+C um alle Services zu stoppen..."
echo "" echo ""
# Trap zum Beenden aller Prozesse # 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 # Warte auf Beendigung
wait wait