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:
@@ -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)")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
start-dev.sh
12
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
|
||||
|
||||
Reference in New Issue
Block a user