Immer noch nicht richtig gut, also noch **WIP**

This commit is contained in:
rxf
2026-03-23 22:09:51 +01:00
parent c471c0e33a
commit acd509fef6
8 changed files with 969 additions and 86 deletions

514
api/README.md Normal file
View File

@@ -0,0 +1,514 @@
# Wetterstation API
REST API zum Abrufen von Wetterdaten aus der PostgreSQL-Datenbank.
## Übersicht
Die API basiert auf **FastAPI** und bietet Endpunkte für aktuelle Wetterdaten, historische Zeitreihen, Statistiken und aggregierte Daten.
- **Version:** 1.0.0
- **Framework:** FastAPI mit Uvicorn
- **Datenbank:** PostgreSQL
- **Interaktive API-Dokumentation:** `/docs` (Swagger UI) oder `/redoc` (ReDoc)
## Starten der API
### Lokal (Development)
```bash
cd api
python main.py
```
Die API läuft dann auf `http://localhost:8000`
### Docker (Production)
```bash
docker compose up -d
```
## Umgebungsvariablen
Die API benötigt folgende Umgebungsvariablen (definiert in `.env`):
```env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=wetterstation
DB_USER=wetterstation_user
DB_PASSWORD=<passwort>
```
## Endpunkte
### 📋 General
#### `GET /`
**Root-Endpunkt mit API-Informationen**
**Response:**
```json
{
"message": "Wetterstation API",
"version": "1.0.0",
"docs": "/docs"
}
```
---
#### `GET /health`
**Health Check - Prüft API- und Datenbankstatus**
**Response:**
```json
{
"status": "ok",
"database": "connected",
"timestamp": "2026-03-23T14:30:00"
}
```
---
### 🌡️ Weather Data
#### `GET /weather/latest`
**Gibt die neuesten Wetterdaten zurück**
**Response Model:** `WeatherData`
**Beispiel:**
```json
{
"id": 123456,
"datetime": "2026-03-23T14:30:00Z",
"temperature": 15.5,
"humidity": 65,
"pressure": 1013.2,
"wind_speed": 12.5,
"wind_gust": 18.7,
"wind_dir": 225.0,
"rain": 0.0,
"rain_rate": 0.0,
"received_at": "2026-03-23T14:30:05"
}
```
---
#### `GET /weather/current`
**Alias für `/weather/latest` - gibt aktuelle Wetterdaten zurück**
---
#### `GET /weather/history`
**Gibt historische Wetterdaten der letzten X Stunden zurück**
**Query Parameter:**
- `hours` (optional): Anzahl Stunden zurück (1-168, default: 24)
- `limit` (optional): Maximale Anzahl Datensätze (1-10000, default: 1000)
**Beispiel:**
```bash
GET /weather/history?hours=48&limit=500
```
**Response:** Array von `WeatherData`
---
#### `GET /weather/range`
**Gibt Wetterdaten für einen bestimmten Zeitraum zurück**
**Query Parameter:**
- `start` (erforderlich): Startdatum (ISO 8601)
- `end` (erforderlich): Enddatum (ISO 8601)
- `limit` (optional): Maximale Anzahl Datensätze (1-50000, default: 10000)
**Beispiel:**
```bash
GET /weather/range?start=2026-03-01T00:00:00Z&end=2026-03-23T23:59:59Z&limit=5000
```
**Response:** Array von `WeatherData`
---
#### `GET /weather/temperature`
**Gibt nur Temperatur-Zeitreihen zurück (optimiert für Diagramme)**
**Query Parameter:**
- `hours` (optional): Anzahl Stunden zurück (1-168, default: 24)
**Response:**
```json
[
{
"datetime": "2026-03-23T14:00:00Z",
"temperature": 15.3
},
{
"datetime": "2026-03-23T14:05:00Z",
"temperature": 15.5
}
]
```
---
#### `GET /weather/wind`
**Gibt nur Wind-Daten zurück (Geschwindigkeit, Richtung, Böen)**
**Query Parameter:**
- `hours` (optional): Anzahl Stunden zurück (1-168, default: 24)
**Response:**
```json
[
{
"datetime": "2026-03-23T14:00:00Z",
"wind_speed": 12.5,
"wind_gust": 18.7,
"wind_dir": 225.0
}
]
```
---
#### `GET /weather/rain`
**Gibt nur Regen-Daten zurück**
**Query Parameter:**
- `hours` (optional): Anzahl Stunden zurück (1-168, default: 24)
**Response:**
```json
[
{
"datetime": "2026-03-23T14:00:00Z",
"rain": 0.5,
"rain_rate": 2.3
}
]
```
---
### 📊 Statistics
#### `GET /weather/stats`
**Gibt aggregierte Statistiken für den angegebenen Zeitraum zurück**
**Query Parameter:**
- `hours` (optional): Zeitraum in Stunden (1-168, default: 24)
**Response Model:** `WeatherStats`
**Beispiel:**
```json
{
"avg_temperature": 15.2,
"min_temperature": 8.5,
"max_temperature": 22.1,
"avg_humidity": 65.3,
"avg_pressure": 1013.5,
"avg_wind_speed": 10.2,
"max_wind_gust": 28.5,
"total_rain": 3.2,
"data_points": 288
}
```
---
#### `GET /weather/daily`
**Gibt tägliche Statistiken für die letzten X Tage zurück**
**Query Parameter:**
- `days` (optional): Anzahl Tage zurück (1-90, default: 7)
**Response:** Array von `WeatherStats` mit `date` Feld
---
### 📈 Aggregated Data
Die aggregierten Endpunkte sind optimiert für Langzeit-Visualisierungen und reduzieren die Datenmenge durch Mittelwertbildung.
#### `GET /weather/hourly-aggregated`
**Gibt stündlich aggregierte Wetterdaten zurück (Stundenmittel)**
**Query Parameter:**
- `days` (optional): Anzahl Tage zurück (1-60, default: 7)
**Response:** Array von `WeatherData` (stündlich aggregiert)
**Verwendung:** Ideal für 7-Tage- und 30-Tage-Ansichten
---
#### `GET /weather/daily-aggregated`
**Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel)**
**Query Parameter:**
- `days` (optional): Anzahl Tage zurück (1-730, default: 365)
**Response:** Array von `WeatherData` (täglich aggregiert)
**Besonderheit:** Bei `days >= 365` werden automatisch **alle verfügbaren Daten** zurückgegeben (nicht nur die letzten 365 Tage).
**Verwendung:** Ideal für Jahresübersicht (365-Tage-Ansicht)
---
#### `GET /weather/rain-daily`
**Gibt tägliche Regensummen zurück**
**Query Parameter:**
- `days` (optional): Anzahl Tage zurück (1-365, default: 30)
**Response:**
```json
[
{
"date": "2026-03-23T00:00:00Z",
"total_rain": 5.2
},
{
"date": "2026-03-22T00:00:00Z",
"total_rain": 0.0
}
]
```
**Verwendung:** Ideal für 7-Tage- und 30-Tage-Regen-Diagramme
---
#### `GET /weather/rain-weekly`
**Gibt wöchentliche Regensummen zurück (Woche = Mo-So)**
**Query Parameter:**
- `days` (optional): Anzahl Tage zurück (1-730, default: 365)
**Response:**
```json
[
{
"week_start": "2026-03-17T00:00:00Z",
"total_rain": 12.5
}
]
```
**Besonderheit:** Bei `days >= 365` werden automatisch **alle verfügbaren Daten** zurückgegeben.
**Verwendung:** Ideal für Jahresübersicht (365-Tage-Ansicht)
---
## Datenmodelle
### WeatherData
```typescript
{
id: number
datetime: string (ISO 8601)
temperature: number | null // °C
humidity: number | null // %
pressure: number | null // hPa
wind_speed: number | null // km/h (konvertiert von mph)
wind_gust: number | null // km/h (konvertiert von mph)
wind_dir: number | null // Grad (0-360)
rain: number | null // mm
rain_rate: number | null // mm/h
received_at: string (ISO 8601)
}
```
### WeatherStats
```typescript
{
avg_temperature: number | null
min_temperature: number | null
max_temperature: number | null
avg_humidity: number | null
avg_pressure: number | null
avg_wind_speed: number | null
max_wind_gust: number | null
total_rain: number | null
data_points: number
}
```
### HealthResponse
```typescript
{
status: string // "ok" | "error"
database: string // "connected" | "disconnected"
timestamp: string (ISO 8601)
}
```
---
## Einheitenkonvertierung
Die API konvertiert automatisch folgende Einheiten aus der Datenbank:
| Wert | Datenbank | API-Ausgabe |
|------|-----------|-------------|
| Windgeschwindigkeit | mph | km/h (× 1.60934) |
| Windböen | mph | km/h (× 1.60934) |
| Temperatur | °C | °C (unverändert) |
| Luftdruck | hPa | hPa (unverändert) |
| Regen | mm | mm (unverändert) |
---
## CORS
Die API erlaubt CORS-Anfragen von allen Origins (`allow_origins=["*"]`). In Production sollte dies auf spezifische Domains eingeschränkt werden.
---
## Fehlerbehandlung
### HTTP Status Codes
- `200 OK` - Erfolgreiche Anfrage
- `400 Bad Request` - Ungültige Parameter
- `404 Not Found` - Keine Daten gefunden
- `500 Internal Server Error` - Datenbankfehler
### Fehler-Response
```json
{
"detail": "Keine Daten verfügbar"
}
```
---
## Interaktive Dokumentation
FastAPI generiert automatisch eine interaktive API-Dokumentation:
- **Swagger UI:** [http://localhost:8000/docs](http://localhost:8000/docs)
- **ReDoc:** [http://localhost:8000/redoc](http://localhost:8000/redoc)
Dort können alle Endpunkte direkt getestet werden.
---
## Beispiele
### cURL
```bash
# Aktuelle Wetterdaten abrufen
curl http://localhost:8000/weather/current
# Letzte 48 Stunden
curl "http://localhost:8000/weather/history?hours=48"
# Jahresübersicht (alle verfügbaren Daten)
curl "http://localhost:8000/weather/daily-aggregated?days=365"
# Statistiken für letzte 7 Tage
curl "http://localhost:8000/weather/stats?hours=168"
```
### JavaScript (Fetch)
```javascript
// Aktuelle Wetterdaten
const response = await fetch('http://localhost:8000/weather/current')
const data = await response.json()
console.log(`Temperatur: ${data.temperature}°C`)
// Tägliche Aggregation für 365 Tage
const yearData = await fetch('http://localhost:8000/weather/daily-aggregated?days=365')
const year = await yearData.json()
console.log(`${year.length} Tage verfügbar`)
```
### Python (requests)
```python
import requests
# Aktuelle Daten
response = requests.get('http://localhost:8000/weather/current')
data = response.json()
print(f"Temperatur: {data['temperature']}°C")
# Statistiken
stats = requests.get('http://localhost:8000/weather/stats?hours=24')
print(f"Durchschnittstemperatur: {stats.json()['avg_temperature']}°C")
```
---
## Entwicklung
### Abhängigkeiten installieren
```bash
pip install -r requirements.txt
```
### Server starten (Development mit Auto-Reload)
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
### Logging
Die API verwendet Python's `logging`-Modul. Log-Level: `INFO`
---
## Deployment
Die API wird als Docker-Container deployed. Siehe `Dockerfile` und `docker-compose.yml` im Hauptverzeichnis.
### Docker Image bauen
```bash
docker build -t wetterstation-api ./api
```
### Container starten
```bash
docker run -d \
-p 8000:8000 \
-e DB_HOST=db \
-e DB_USER=wetterstation_user \
-e DB_PASSWORD=<passwort> \
wetterstation-api
```
---
## Performance-Tipps
1. **Aggregierte Endpunkte verwenden** für Langzeit-Visualisierungen (reduziert Datenmenge)
2. **Limit-Parameter** nutzen, um nur benötigte Datenmenge abzurufen
3. **Spezifische Endpunkte** verwenden (`/weather/temperature` statt `/weather/history` wenn nur Temperatur benötigt wird)
4. **Caching** auf Client-Seite implementieren für historische Daten
---
## Lizenz
Siehe Hauptprojekt-Repository.

View File

@@ -383,6 +383,26 @@ async def get_daily_aggregated_data(
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
if days >= 365:
cursor.execute("""
SELECT
0 as id,
date_trunc('day', datetime) as datetime,
AVG(temperature) as temperature,
ROUND(AVG(humidity)) as humidity,
AVG(pressure) as pressure,
AVG(wind_speed * 1.60934) as wind_speed,
MAX(wind_gust * 1.60934) as wind_gust,
AVG(wind_dir) as wind_dir,
AVG(rain) as rain,
AVG(rain_rate) as rain_rate,
MAX(received_at) as received_at
FROM weather_data
GROUP BY date_trunc('day', datetime)
ORDER BY datetime ASC
""")
else:
cursor.execute("""
SELECT
0 as id,
@@ -440,6 +460,17 @@ async def get_weekly_rain_data(
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
if days >= 365:
cursor.execute("""
SELECT
date_trunc('week', datetime) as week_start,
SUM(rain) as total_rain
FROM weather_data
GROUP BY date_trunc('week', datetime)
ORDER BY week_start ASC
""")
else:
cursor.execute("""
SELECT
date_trunc('week', datetime) as week_start,

BIN
data/365.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
data/wview-archive.sdb Normal file

Binary file not shown.

View File

@@ -0,0 +1,41 @@
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: wetterstation_db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "${DB_PORT}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
pgadmin:
image: dpage/pgadmin4:latest
container_name: wetterstation_pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@admin.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
PGADMIN_CONFIG_SERVER_MODE: 'False'
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
depends_on:
- postgres
volumes:
postgres_data:
driver: local
pgadmin_data:
driver: local

View File

@@ -1,6 +1,7 @@
.dashboard {
width: 100%;
max-width: 1900px;
/* max-width: 1900px; */
max-width: 795px;
margin: 0 auto;
}

View File

@@ -37,6 +37,19 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
}
}, [timeRange])
// Aggregations-Zusatz für Chart-Titel
const aggregationSuffix = useMemo(() => {
switch (timeRange) {
case '7d':
case '30d':
return ' (Stundenmittel)'
case '365d':
return ' (Tagesmittel)'
default:
return ''
}
}, [timeRange])
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
const getCommonOptions = () => {
// X-Achsen-Konfiguration basierend auf Zeitraum
@@ -46,20 +59,44 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
gridLineColor: 'rgba(0, 0, 0, 0.1)'
}
// Zeitspanne für X-Achse berechnen (für festen Zeitrahmen)
const now = new Date().getTime()
let xAxisMin, xAxisMax
switch (timeRange) {
case '24h':
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
xAxisMin = now - 24 * 3600 * 1000
xAxisMax = now
break
case '7d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 7 * 24 * 3600 * 1000
xAxisMax = now
break
case '30d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 30 * 24 * 3600 * 1000
xAxisMax = now
break
case '365d':
xAxisConfig.labels = { format: '{value:%b}', align: 'center' }
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
if (sortedData.length > 0) {
xAxisMin = new Date(sortedData[0].datetime).getTime()
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
} else {
xAxisMin = null
xAxisMax = null
}
break
}
// Min/Max für X-Achse setzen
xAxisConfig.min = xAxisMin
xAxisConfig.max = xAxisMax
return {
chart: {
height: '50%',
@@ -105,7 +142,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
// Temperatur Chart
const temperatureOptions = useMemo(() => {
const temps = sortedData.map(item => item.temperature)
const temps = sortedData.filter(item => item.temperature != null).map(item => item.temperature)
const min = Math.min(...temps)
const max = Math.max(...temps)
const range = max - min
@@ -129,7 +166,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
},
series: [{
name: 'Temperatur',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.temperature]),
data: sortedData.filter(item => item.temperature != null).map(item => [new Date(item.datetime).getTime(), item.temperature]),
color: 'rgb(255, 99, 132)',
fillColor: {
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
@@ -140,12 +177,15 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
},
type: 'areaspline',
threshold: null,
connectNulls: false,
gapSize: 2 * 24 * 3600 * 1000,
gapUnit: 'value',
tooltip: {
valueSuffix: ' °C'
}
}]
}
}, [sortedData])
}, [sortedData, aggregationSuffix])
// Luftfeuchtigkeit Chart
const humidityOptions = useMemo(() => ({
@@ -158,7 +198,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
},
series: [{
name: 'Feuchte',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.humidity]),
data: sortedData.filter(item => item.humidity != null).map(item => [new Date(item.datetime).getTime(), item.humidity]),
color: 'rgb(54, 162, 235)',
fillColor: {
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
@@ -168,6 +208,9 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
]
},
type: 'area',
connectNulls: false,
gapSize: 2 * 24 * 3600 * 1000,
gapUnit: 'value',
tooltip: {
valueSuffix: ' %'
}
@@ -176,7 +219,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
// Luftdruck Chart
const pressureOptions = useMemo(() => {
const pressures = sortedData.map(item => item.pressure)
const pressures = sortedData.filter(item => item.pressure != null).map(item => item.pressure)
const min = Math.min(...pressures)
const max = Math.max(...pressures)
const range = max - min
@@ -200,7 +243,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
},
series: [{
name: 'Luftdruck',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.pressure]),
data: sortedData.filter(item => item.pressure != null).map(item => [new Date(item.datetime).getTime(), item.pressure]),
color: 'rgb(75, 192, 192)',
fillColor: {
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
@@ -210,6 +253,9 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
]
},
type: 'area',
connectNulls: false,
gapSize: 2 * 24 * 3600 * 1000,
gapUnit: 'value',
tooltip: {
valueSuffix: ' hPa'
}
@@ -227,7 +273,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
yAxisTitle = 'Regen (mm) / Rate (mm/h)'
series = [{
name: 'Regen',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
data: sortedData.filter(item => item.rain != null).map(item => [new Date(item.datetime).getTime(), item.rain]),
color: 'rgb(54, 162, 235)',
fillColor: 'rgba(54, 162, 235, 0.3)',
type: 'area',
@@ -236,7 +282,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
}
}, {
name: 'Regenrate',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
data: sortedData.filter(item => item.rain_rate != null).map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
color: 'rgb(59, 130, 246)',
dashStyle: 'Dash',
type: 'line',
@@ -249,7 +295,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
yAxisTitle = 'Regen (mm pro Tag)'
series = [{
name: 'Regen',
data: rainData.map(item => [new Date(item.date).getTime(), item.total_rain || 0]),
data: rainData.filter(item => item.total_rain != null && item.total_rain > 0).map(item => [new Date(item.date).getTime(), item.total_rain]),
color: 'rgb(54, 162, 235)',
type: 'column',
tooltip: {
@@ -261,7 +307,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
yAxisTitle = 'Regen (mm pro Woche)'
series = [{
name: 'Regen',
data: rainData.map(item => [new Date(item.week_start).getTime(), item.total_rain || 0]),
data: rainData.filter(item => item.total_rain != null && item.total_rain > 0).map(item => [new Date(item.week_start).getTime(), item.total_rain]),
color: 'rgb(54, 162, 235)',
type: 'column',
tooltip: {
@@ -281,7 +327,55 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
}, [sortedData, rainData, timeRange])
// Windgeschwindigkeit Chart
const windSpeedOptions = useMemo(() => ({
const windSpeedOptions = useMemo(() => {
// Bei 365d nur Windgeschwindigkeit, keine Böen
const series = timeRange === '365d'
? [{
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: {
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: {
valueSuffix: ' km/h'
}
}, {
name: 'Windböen',
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)',
type: 'area',
connectNulls: false,
gapSize: 2 * 24 * 3600 * 1000,
gapUnit: 'value',
tooltip: {
valueSuffix: ' km/h'
}
}]
return {
...getCommonOptions(),
plotOptions: {
series: {
@@ -303,26 +397,9 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
}
}
},
series: [{
name: 'Windgeschwindigkeit',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
color: 'rgb(153, 102, 255)',
fillColor: 'rgba(153, 102, 255, 0.1)',
type: 'area',
tooltip: {
valueSuffix: ' km/h'
series
}
}, {
name: 'Windböen',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_gust]),
color: 'rgb(255, 159, 64)',
fillColor: 'rgba(255, 159, 64, 0.1)',
type: 'area',
tooltip: {
valueSuffix: ' km/h'
}
}]
}), [sortedData])
}, [sortedData, timeRange])
// Windrichtung Chart
const windDirOptions = useMemo(() => ({
@@ -359,7 +436,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
},
series: [{
name: 'Windrichtung',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_dir]),
data: sortedData.filter(item => item.wind_dir != null).map(item => [new Date(item.datetime).getTime(), item.wind_dir]),
color: 'rgb(54, 162, 235)',
type: 'scatter',
tooltip: {
@@ -465,7 +542,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
{/* Charts Grid */}
<div className="charts-grid">
<div className="chart-container">
<h3>🌡 Temperatur - Aktuell: {current.temperature?.toFixed(1) || '-'}°C</h3>
<h3>🌡 Temperatur{aggregationSuffix} - Aktuell: {current.temperature?.toFixed(1) || '-'}°C</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
</div>
@@ -475,7 +552,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
</div>
<div className="chart-container">
<h3>🌐 Luftdruck - Aktuell: {current.pressure?.toFixed(1) || '-'} hPa</h3>
<h3>🌐 Luftdruck{aggregationSuffix} - Aktuell: {current.pressure?.toFixed(1) || '-'} hPa</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
</div>
@@ -485,7 +562,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
</div>
<div className="chart-container">
<h3>💧 Luftfeuchtigkeit - Aktuell: {current.humidity || '-'}%</h3>
<h3>💧 Luftfeuchtigkeit{aggregationSuffix} - Aktuell: {current.humidity || '-'}%</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
</div>
@@ -495,21 +572,21 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
</div>
<div className="chart-container">
<h3>🌧 Regen - Aktuell: {current.rain?.toFixed(1) || '-'} mm</h3>
<h3>🌧 Regen{aggregationSuffix} - Aktuell: {current.rain?.toFixed(1) || '-'} mm</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
</div>
</div>
<div className="chart-container">
<h3>🧭 Windrichtung - Aktuell: {current.wind_dir ?? '-'}°</h3>
<h3>🧭 Windrichtung{aggregationSuffix} - Aktuell: {current.wind_dir ?? '-'}°</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={windDirOptions} />
</div>
</div>
<div className="chart-container">
<h3>💨 Windspeed - Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</h3>
<h3>💨 Windspeed{aggregationSuffix} - Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div>

219
migrate_sqlite_to_postgres.py Executable file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
Migration Tool: SQLite (wview) → PostgreSQL (wetterstation)
Migriert Wetterdaten vom 1.1.2025 bis heute
"""
import sqlite3
import psycopg
from datetime import datetime, timezone
import os
from pathlib import Path
from dotenv import load_dotenv
import sys
# Umgebungsvariablen laden
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)
# Konfiguration
SQLITE_DB = "data/wview-archive.sdb"
START_DATE = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
END_DATE = datetime(2026, 2, 8, 0, 0, 0, tzinfo=timezone.utc)
# PostgreSQL-Konfiguration
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', 5432))
DB_NAME = os.getenv('DB_NAME', 'wetterstation')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
# Soll die Tabelle vorher geleert werden?
TRUNCATE_TABLE = False # Auf False setzen, um vorhandene Daten zu behalten
# Konvertierungsfunktionen
def fahrenheit_to_celsius(f):
"""Fahrenheit → Celsius"""
if f is None:
return None
return (f - 32) * 5 / 9
def inches_hg_to_hpa(inhg):
"""inches Hg → hPa"""
if inhg is None:
return None
return inhg * 33.8639
def mph_to_kmh(mph):
"""mph → km/h"""
if mph is None:
return None
return mph * 1.60934
def inches_to_mm(inches):
"""inches → mm"""
if inches is None:
return None
return inches * 25.4
def unix_to_datetime(timestamp):
"""Unix timestamp → datetime"""
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
def main():
print("=" * 60)
print("SQLite → PostgreSQL Migration")
print("=" * 60)
print(f"Quelle: {SQLITE_DB}")
print(f"Zeitraum: {START_DATE.date()} bis {END_DATE.date()}")
print(f"Ziel: PostgreSQL ({DB_HOST}:{DB_PORT}/{DB_NAME})")
print("=" * 60)
print()
# SQLite öffnen
try:
sqlite_conn = sqlite3.connect(SQLITE_DB)
sqlite_cursor = sqlite_conn.cursor()
print("✓ SQLite-Verbindung hergestellt")
except Exception as e:
print(f"✗ Fehler beim Öffnen der SQLite-Datenbank: {e}")
sys.exit(1)
# PostgreSQL öffnen
try:
pg_conn = psycopg.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
pg_cursor = pg_conn.cursor()
print("✓ PostgreSQL-Verbindung hergestellt")
except Exception as e:
print(f"✗ Fehler beim Verbinden mit PostgreSQL: {e}")
sqlite_conn.close()
sys.exit(1)
# Tabelle leeren falls gewünscht
if TRUNCATE_TABLE:
print("\nLeere PostgreSQL-Tabelle weather_data...")
try:
pg_cursor.execute("TRUNCATE TABLE weather_data RESTART IDENTITY CASCADE")
pg_conn.commit()
print("✓ Tabelle geleert")
except Exception as e:
print(f"✗ Fehler beim Leeren der Tabelle: {e}")
sqlite_conn.close()
pg_conn.close()
sys.exit(1)
# Zeitraum in Unix timestamps umrechnen
start_ts = int(START_DATE.timestamp())
end_ts = int(END_DATE.timestamp())
# Daten aus SQLite laden
print(f"\nLade Daten aus SQLite (Zeitraum: {start_ts} - {end_ts})...")
sqlite_cursor.execute("""
SELECT
dateTime,
outTemp,
outHumidity,
barometer,
windSpeed,
windGust,
windDir,
rain,
rainRate
FROM archive
WHERE dateTime >= ? AND dateTime <= ?
ORDER BY dateTime ASC
""", (start_ts, end_ts))
rows = sqlite_cursor.fetchall()
print(f"{len(rows)} Datensätze gefunden")
if len(rows) == 0:
print("Keine Daten im angegebenen Zeitraum gefunden.")
sqlite_conn.close()
pg_conn.close()
return
# Migration durchführen
print("\nMigriere Daten...")
inserted = 0
skipped = 0
errors = 0
for row in rows:
try:
(dateTime, outTemp, outHumidity, barometer,
windSpeed, windGust, windDir, rain, rainRate) = row
# Konvertierungen
dt = unix_to_datetime(dateTime)
temp_c = fahrenheit_to_celsius(outTemp)
humidity = int(outHumidity) if outHumidity is not None else None
pressure_hpa = inches_hg_to_hpa(barometer)
wind_speed_kmh = mph_to_kmh(windSpeed)
wind_gust_kmh = mph_to_kmh(windGust)
rain_mm = inches_to_mm(rain)
rain_rate_mm = inches_to_mm(rainRate)
# In PostgreSQL einfügen
pg_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)
ON CONFLICT (datetime) DO NOTHING
""", (dt, temp_c, humidity, pressure_hpa,
wind_speed_kmh, wind_gust_kmh, windDir, rain_mm, rain_rate_mm))
if pg_cursor.rowcount > 0:
inserted += 1
if inserted % 1000 == 0:
pg_conn.commit()
print(f" {inserted} Datensätze eingefügt...")
else:
skipped += 1
except Exception as e:
errors += 1
if errors <= 5: # Zeige nur die ersten 5 Fehler
print(f" Fehler bei Datensatz {dateTime}: {e}")
# Commit verbleibende Daten
pg_conn.commit()
# Zusammenfassung
print("\n" + "=" * 60)
print("Migration abgeschlossen!")
print("=" * 60)
print(f"Eingefügt: {inserted} Datensätze")
print(f"Übersprungen: {skipped} Datensätze (bereits vorhanden)")
print(f"Fehler: {errors} Datensätze")
print("=" * 60)
# Zeitraum der migrierten Daten anzeigen
if inserted > 0:
pg_cursor.execute("""
SELECT MIN(datetime), MAX(datetime), COUNT(*)
FROM weather_data
WHERE datetime >= %s AND datetime <= %s
""", (START_DATE, END_DATE))
min_dt, max_dt, count = pg_cursor.fetchone()
print(f"\nDaten in PostgreSQL:")
print(f" Von: {min_dt}")
print(f" Bis: {max_dt}")
print(f" Gesamt: {count} Datensätze")
# Verbindungen schließen
sqlite_conn.close()
pg_conn.close()
print("\n✓ Fertig!")
if __name__ == "__main__":
main()