Temperatur mit Min/Max
This commit is contained in:
71
api/main.py
71
api/main.py
@@ -375,11 +375,11 @@ async def get_hourly_aggregated_data(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/weather/daily-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
|
@app.get("/weather/daily-aggregated", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
async def get_daily_aggregated_data(
|
async def get_daily_aggregated_data(
|
||||||
days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück (max 730)")
|
days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück (max 730)")
|
||||||
):
|
):
|
||||||
"""Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel)"""
|
"""Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel mit Min/Max-Temperaturen)"""
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
@@ -387,17 +387,15 @@ async def get_daily_aggregated_data(
|
|||||||
if days >= 365:
|
if days >= 365:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
0 as id,
|
|
||||||
date_trunc('day', datetime) as datetime,
|
date_trunc('day', datetime) as datetime,
|
||||||
AVG(temperature) as temperature,
|
AVG(temperature)::float as temperature,
|
||||||
ROUND(AVG(humidity)) as humidity,
|
MIN(temperature)::float as min_temperature,
|
||||||
AVG(pressure) as pressure,
|
MAX(temperature)::float as max_temperature,
|
||||||
AVG(wind_speed * 1.60934) as wind_speed,
|
ROUND(AVG(humidity))::int as humidity,
|
||||||
MAX(wind_gust * 1.60934) as wind_gust,
|
AVG(pressure)::float as pressure,
|
||||||
AVG(wind_dir) as wind_dir,
|
AVG(wind_speed * 1.60934)::float as wind_speed,
|
||||||
AVG(rain) as rain,
|
MAX(wind_gust * 1.60934)::float as wind_gust,
|
||||||
AVG(rain_rate) as rain_rate,
|
AVG(wind_dir)::float as wind_dir
|
||||||
MAX(received_at) as received_at
|
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
GROUP BY date_trunc('day', datetime)
|
GROUP BY date_trunc('day', datetime)
|
||||||
ORDER BY datetime ASC
|
ORDER BY datetime ASC
|
||||||
@@ -405,17 +403,15 @@ async def get_daily_aggregated_data(
|
|||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
0 as id,
|
|
||||||
date_trunc('day', datetime) as datetime,
|
date_trunc('day', datetime) as datetime,
|
||||||
AVG(temperature) as temperature,
|
AVG(temperature)::float as temperature,
|
||||||
ROUND(AVG(humidity)) as humidity,
|
MIN(temperature)::float as min_temperature,
|
||||||
AVG(pressure) as pressure,
|
MAX(temperature)::float as max_temperature,
|
||||||
AVG(wind_speed * 1.60934) as wind_speed,
|
ROUND(AVG(humidity))::int as humidity,
|
||||||
MAX(wind_gust * 1.60934) as wind_gust,
|
AVG(pressure)::float as pressure,
|
||||||
AVG(wind_dir) as wind_dir,
|
AVG(wind_speed * 1.60934)::float as wind_speed,
|
||||||
AVG(rain) as rain,
|
MAX(wind_gust * 1.60934)::float as wind_gust,
|
||||||
AVG(rain_rate) as rain_rate,
|
AVG(wind_dir)::float as wind_dir
|
||||||
MAX(received_at) as received_at
|
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
WHERE datetime >= NOW() - make_interval(days => %s)
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
GROUP BY date_trunc('day', datetime)
|
GROUP BY date_trunc('day', datetime)
|
||||||
@@ -428,6 +424,37 @@ async def get_daily_aggregated_data(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/daily-with-minmax", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
|
async def get_daily_with_minmax_data(
|
||||||
|
days: int = Query(30, ge=1, le=90, description="Anzahl Tage zurück (max 90)")
|
||||||
|
):
|
||||||
|
"""Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen zurück"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
date_trunc('day', datetime) as datetime,
|
||||||
|
AVG(temperature)::float as temperature,
|
||||||
|
MIN(temperature)::float as min_temperature,
|
||||||
|
MAX(temperature)::float as max_temperature,
|
||||||
|
ROUND(AVG(humidity))::int as humidity,
|
||||||
|
AVG(pressure)::float as pressure,
|
||||||
|
AVG(wind_speed * 1.60934)::float as wind_speed,
|
||||||
|
MAX(wind_gust * 1.60934)::float as wind_gust,
|
||||||
|
AVG(wind_dir)::float as wind_dir
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('day', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (days,))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/weather/rain-daily", response_model=List[dict], tags=["Aggregated Data"])
|
@app.get("/weather/rain-daily", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
async def get_daily_rain_data(
|
async def get_daily_rain_data(
|
||||||
days: int = Query(30, ge=1, le=365, description="Anzahl Tage zurück")
|
days: int = Query(30, ge=1, le=365, description="Anzahl Tage zurück")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function App() {
|
|||||||
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
|
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
|
||||||
break
|
break
|
||||||
case '30d':
|
case '30d':
|
||||||
weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=30`
|
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=30`
|
||||||
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
|
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
|
||||||
break
|
break
|
||||||
case '365d':
|
case '365d':
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
const aggregationSuffix = useMemo(() => {
|
const aggregationSuffix = useMemo(() => {
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '7d':
|
case '7d':
|
||||||
case '30d':
|
|
||||||
return ' (Stundenmittel)'
|
return ' (Stundenmittel)'
|
||||||
|
case '30d':
|
||||||
case '365d':
|
case '365d':
|
||||||
return ' (Tagesmittel)'
|
return ' (Tagesmittel)'
|
||||||
default:
|
default:
|
||||||
@@ -55,6 +55,30 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
}, [timeRange])
|
}, [timeRange])
|
||||||
|
|
||||||
|
// Spezieller Suffix für Temperatur bei 30d/365d
|
||||||
|
const temperatureSuffix = useMemo(() => {
|
||||||
|
switch (timeRange) {
|
||||||
|
case '7d':
|
||||||
|
return ' (Stundenmittel)'
|
||||||
|
case '30d':
|
||||||
|
case '365d':
|
||||||
|
return ' (Tages-Min/Max)'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}, [timeRange])
|
||||||
|
|
||||||
|
// Spezieller Suffix für Windböen bei 30d/365d
|
||||||
|
const windGustSuffix = useMemo(() => {
|
||||||
|
switch (timeRange) {
|
||||||
|
case '30d':
|
||||||
|
case '365d':
|
||||||
|
return ' (TagesMax)'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}, [timeRange])
|
||||||
|
|
||||||
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
||||||
const getCommonOptions = () => {
|
const getCommonOptions = () => {
|
||||||
// X-Achsen-Konfiguration basierend auf Zeitraum
|
// X-Achsen-Konfiguration basierend auf Zeitraum
|
||||||
@@ -123,7 +147,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
shared: true,
|
shared: true,
|
||||||
crosshairs: true,
|
crosshairs: true,
|
||||||
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' || timeRange === '30d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y')
|
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y')
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
series: {
|
series: {
|
||||||
@@ -147,7 +171,87 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Temperatur Chart
|
// Temperatur Chart
|
||||||
const temperatureOptions = useMemo(() => {
|
const temperatureOptions = useMemo(() => {
|
||||||
|
// Bei 30d und 365d: Min/Max-Temperaturen anzeigen
|
||||||
|
if (timeRange === '30d' || timeRange === '365d') {
|
||||||
|
const minTemps = sortedData.filter(item => item.min_temperature != null).map(item => item.min_temperature)
|
||||||
|
const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature)
|
||||||
|
|
||||||
|
// Prüfe, ob Daten vorhanden sind
|
||||||
|
if (minTemps.length === 0 || maxTemps.length === 0) {
|
||||||
|
return {
|
||||||
|
...getCommonOptions(),
|
||||||
|
yAxis: {
|
||||||
|
...getCommonOptions().yAxis,
|
||||||
|
title: { text: null }
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let yMin = Math.min(...minTemps)
|
||||||
|
let yMax = Math.max(...maxTemps)
|
||||||
|
|
||||||
|
// Füge einen kleinen Puffer hinzu (2°C oben/unten), um den Platz optimal zu nutzen
|
||||||
|
yMin = Math.floor(yMin - 2)
|
||||||
|
yMax = Math.ceil(yMax + 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getCommonOptions(),
|
||||||
|
yAxis: {
|
||||||
|
title: { text: null },
|
||||||
|
gridLineColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
min: yMin,
|
||||||
|
max: yMax,
|
||||||
|
startOnTick: false,
|
||||||
|
endOnTick: false
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Maximaltemperatur',
|
||||||
|
data: sortedData.filter(item => item.max_temperature != null).map(item => [new Date(item.datetime).getTime(), item.max_temperature]),
|
||||||
|
color: 'rgb(255, 99, 132)',
|
||||||
|
type: 'line',
|
||||||
|
lineWidth: 2,
|
||||||
|
connectNulls: false,
|
||||||
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
|
gapUnit: 'value',
|
||||||
|
tooltip: {
|
||||||
|
valueDecimals: 1,
|
||||||
|
valueSuffix: ' °C'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Minimaltemperatur',
|
||||||
|
data: sortedData.filter(item => item.min_temperature != null).map(item => [new Date(item.datetime).getTime(), item.min_temperature]),
|
||||||
|
color: 'rgb(54, 162, 235)',
|
||||||
|
type: 'line',
|
||||||
|
lineWidth: 2,
|
||||||
|
connectNulls: false,
|
||||||
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
|
gapUnit: 'value',
|
||||||
|
tooltip: {
|
||||||
|
valueDecimals: 1,
|
||||||
|
valueSuffix: ' °C'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard: Temperatur als Flächendiagramm
|
||||||
const temps = sortedData.filter(item => item.temperature != null).map(item => item.temperature)
|
const temps = sortedData.filter(item => item.temperature != null).map(item => item.temperature)
|
||||||
|
|
||||||
|
if (temps.length === 0) {
|
||||||
|
return {
|
||||||
|
...getCommonOptions(),
|
||||||
|
yAxis: {
|
||||||
|
...getCommonOptions().yAxis,
|
||||||
|
title: { text: null }
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const min = Math.min(...temps)
|
const min = Math.min(...temps)
|
||||||
const max = Math.max(...temps)
|
const max = Math.max(...temps)
|
||||||
const range = max - min
|
const range = max - min
|
||||||
@@ -191,7 +295,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}, [sortedData, aggregationSuffix])
|
}, [sortedData, temperatureSuffix, timeRange])
|
||||||
|
|
||||||
// Luftfeuchtigkeit Chart
|
// Luftfeuchtigkeit Chart
|
||||||
const humidityOptions = useMemo(() => ({
|
const humidityOptions = useMemo(() => ({
|
||||||
@@ -227,6 +331,18 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
// Luftdruck Chart
|
// Luftdruck Chart
|
||||||
const pressureOptions = useMemo(() => {
|
const pressureOptions = useMemo(() => {
|
||||||
const pressures = sortedData.filter(item => item.pressure != null).map(item => item.pressure)
|
const pressures = sortedData.filter(item => item.pressure != null).map(item => item.pressure)
|
||||||
|
|
||||||
|
if (pressures.length === 0) {
|
||||||
|
return {
|
||||||
|
...getCommonOptions(),
|
||||||
|
yAxis: {
|
||||||
|
...getCommonOptions().yAxis,
|
||||||
|
title: { text: null }
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const min = Math.min(...pressures)
|
const min = Math.min(...pressures)
|
||||||
const max = Math.max(...pressures)
|
const max = Math.max(...pressures)
|
||||||
const range = max - min
|
const range = max - min
|
||||||
@@ -370,7 +486,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
valueSuffix: ' km/h'
|
valueSuffix: ' km/h'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
name: 'Windböen',
|
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]),
|
||||||
@@ -405,7 +521,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
},
|
},
|
||||||
series
|
series
|
||||||
}
|
}
|
||||||
}, [sortedData, timeRange])
|
}, [sortedData, timeRange, windGustSuffix])
|
||||||
|
|
||||||
// Windrichtung Chart
|
// Windrichtung Chart
|
||||||
const windDirOptions = useMemo(() => ({
|
const windDirOptions = useMemo(() => ({
|
||||||
@@ -558,7 +674,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
<div className="chart-item">
|
<div className="chart-item">
|
||||||
<div className="current-value">Aktuell: {current.temperature?.toFixed(1) || '-'}°C</div>
|
<div className="current-value">Aktuell: {current.temperature?.toFixed(1) || '-'}°C</div>
|
||||||
<div className="chart-container">
|
<div className="chart-container">
|
||||||
<h3><span>🌡️ Temperatur{aggregationSuffix}</span><span className="unit">[°C]</span></h3>
|
<h3><span>🌡️ Temperatur{temperatureSuffix}</span><span className="unit">[°C]</span></h3>
|
||||||
<div className="chart-wrapper">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user