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
|
# 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)")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
// Bei 365d und custom >= 365 Tage: nur Windgeschwindigkeit, keine Böen
|
const windSpeedSeries = {
|
||||||
const series = hideGusts
|
name: 'Windgeschwindigkeit',
|
||||||
? [{
|
data: sortedData
|
||||||
name: 'Windgeschwindigkeit',
|
.filter(item => item.wind_speed != null)
|
||||||
data: sortedData
|
.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
|
||||||
.filter(item => item.wind_speed != null)
|
color: 'rgb(153, 102, 255)',
|
||||||
.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
|
fillColor: 'rgba(153, 102, 255, 0.1)',
|
||||||
color: 'rgb(153, 102, 255)',
|
type: 'area',
|
||||||
fillColor: 'rgba(153, 102, 255, 0.1)',
|
connectNulls: false,
|
||||||
type: 'area',
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
connectNulls: false,
|
gapUnit: 'value',
|
||||||
gapSize: 2 * 24 * 3600 * 1000,
|
tooltip: {
|
||||||
gapUnit: 'value',
|
valueDecimals: 1,
|
||||||
tooltip: {
|
valueSuffix: ' km/h'
|
||||||
valueDecimals: 1,
|
}
|
||||||
valueSuffix: ' km/h'
|
}
|
||||||
}
|
|
||||||
}]
|
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: '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>
|
||||||
|
|||||||
12
start-dev.sh
12
start-dev.sh
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user