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
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)")

View File

@@ -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()

View File

@@ -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 = '
<div className="chart-item">
<div className="current-value">Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</div>
<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">
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div>

View File

@@ -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