Immer noch nicht richtig gut, also noch **WIP**
This commit is contained in:
514
api/README.md
Normal file
514
api/README.md
Normal 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.
|
||||||
85
api/main.py
85
api/main.py
@@ -383,24 +383,44 @@ async def get_daily_aggregated_data(
|
|||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute("""
|
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
|
||||||
SELECT
|
if days >= 365:
|
||||||
0 as id,
|
cursor.execute("""
|
||||||
date_trunc('day', datetime) as datetime,
|
SELECT
|
||||||
AVG(temperature) as temperature,
|
0 as id,
|
||||||
ROUND(AVG(humidity)) as humidity,
|
date_trunc('day', datetime) as datetime,
|
||||||
AVG(pressure) as pressure,
|
AVG(temperature) as temperature,
|
||||||
AVG(wind_speed * 1.60934) as wind_speed,
|
ROUND(AVG(humidity)) as humidity,
|
||||||
MAX(wind_gust * 1.60934) as wind_gust,
|
AVG(pressure) as pressure,
|
||||||
AVG(wind_dir) as wind_dir,
|
AVG(wind_speed * 1.60934) as wind_speed,
|
||||||
AVG(rain) as rain,
|
MAX(wind_gust * 1.60934) as wind_gust,
|
||||||
AVG(rain_rate) as rain_rate,
|
AVG(wind_dir) as wind_dir,
|
||||||
MAX(received_at) as received_at
|
AVG(rain) as rain,
|
||||||
FROM weather_data
|
AVG(rain_rate) as rain_rate,
|
||||||
WHERE datetime >= NOW() - make_interval(days => %s)
|
MAX(received_at) as received_at
|
||||||
GROUP BY date_trunc('day', datetime)
|
FROM weather_data
|
||||||
ORDER BY datetime ASC
|
GROUP BY date_trunc('day', datetime)
|
||||||
""", (days,))
|
ORDER BY datetime ASC
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('day', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (days,))
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
|
|
||||||
return [dict(row) for row in results]
|
return [dict(row) for row in results]
|
||||||
@@ -440,15 +460,26 @@ async def get_weekly_rain_data(
|
|||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute("""
|
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
|
||||||
SELECT
|
if days >= 365:
|
||||||
date_trunc('week', datetime) as week_start,
|
cursor.execute("""
|
||||||
SUM(rain) as total_rain
|
SELECT
|
||||||
FROM weather_data
|
date_trunc('week', datetime) as week_start,
|
||||||
WHERE datetime >= NOW() - make_interval(days => %s)
|
SUM(rain) as total_rain
|
||||||
GROUP BY date_trunc('week', datetime)
|
FROM weather_data
|
||||||
ORDER BY week_start ASC
|
GROUP BY date_trunc('week', datetime)
|
||||||
""", (days,))
|
ORDER BY week_start ASC
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
date_trunc('week', datetime) as week_start,
|
||||||
|
SUM(rain) as total_rain
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('week', datetime)
|
||||||
|
ORDER BY week_start ASC
|
||||||
|
""", (days,))
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
|
|
||||||
return [dict(row) for row in results]
|
return [dict(row) for row in results]
|
||||||
|
|||||||
BIN
data/365.png
Normal file
BIN
data/365.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
data/wview-archive.sdb
Normal file
BIN
data/wview-archive.sdb
Normal file
Binary file not shown.
41
docker-compose_postgres.yml
Normal file
41
docker-compose_postgres.yml
Normal 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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
.dashboard {
|
.dashboard {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1900px;
|
/* max-width: 1900px; */
|
||||||
|
max-width: 795px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
}
|
}
|
||||||
}, [timeRange])
|
}, [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)
|
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
||||||
const getCommonOptions = () => {
|
const getCommonOptions = () => {
|
||||||
// X-Achsen-Konfiguration basierend auf Zeitraum
|
// X-Achsen-Konfiguration basierend auf Zeitraum
|
||||||
@@ -46,20 +59,44 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
gridLineColor: 'rgba(0, 0, 0, 0.1)'
|
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) {
|
switch (timeRange) {
|
||||||
case '24h':
|
case '24h':
|
||||||
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
||||||
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
||||||
|
xAxisMin = now - 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
break
|
break
|
||||||
case '7d':
|
case '7d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
|
xAxisMin = now - 7 * 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
|
break
|
||||||
case '30d':
|
case '30d':
|
||||||
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
|
xAxisMin = now - 30 * 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
break
|
break
|
||||||
case '365d':
|
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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Min/Max für X-Achse setzen
|
||||||
|
xAxisConfig.min = xAxisMin
|
||||||
|
xAxisConfig.max = xAxisMax
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chart: {
|
chart: {
|
||||||
height: '50%',
|
height: '50%',
|
||||||
@@ -105,7 +142,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
|
|
||||||
// Temperatur Chart
|
// Temperatur Chart
|
||||||
const temperatureOptions = useMemo(() => {
|
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 min = Math.min(...temps)
|
||||||
const max = Math.max(...temps)
|
const max = Math.max(...temps)
|
||||||
const range = max - min
|
const range = max - min
|
||||||
@@ -129,7 +166,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Temperatur',
|
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)',
|
color: 'rgb(255, 99, 132)',
|
||||||
fillColor: {
|
fillColor: {
|
||||||
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
||||||
@@ -140,12 +177,15 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
},
|
},
|
||||||
type: 'areaspline',
|
type: 'areaspline',
|
||||||
threshold: null,
|
threshold: null,
|
||||||
|
connectNulls: false,
|
||||||
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
|
gapUnit: 'value',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueSuffix: ' °C'
|
valueSuffix: ' °C'
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}, [sortedData])
|
}, [sortedData, aggregationSuffix])
|
||||||
|
|
||||||
// Luftfeuchtigkeit Chart
|
// Luftfeuchtigkeit Chart
|
||||||
const humidityOptions = useMemo(() => ({
|
const humidityOptions = useMemo(() => ({
|
||||||
@@ -158,7 +198,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Feuchte',
|
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)',
|
color: 'rgb(54, 162, 235)',
|
||||||
fillColor: {
|
fillColor: {
|
||||||
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
||||||
@@ -168,6 +208,9 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
type: 'area',
|
type: 'area',
|
||||||
|
connectNulls: false,
|
||||||
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
|
gapUnit: 'value',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueSuffix: ' %'
|
valueSuffix: ' %'
|
||||||
}
|
}
|
||||||
@@ -176,7 +219,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
|
|
||||||
// Luftdruck Chart
|
// Luftdruck Chart
|
||||||
const pressureOptions = useMemo(() => {
|
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 min = Math.min(...pressures)
|
||||||
const max = Math.max(...pressures)
|
const max = Math.max(...pressures)
|
||||||
const range = max - min
|
const range = max - min
|
||||||
@@ -200,7 +243,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Luftdruck',
|
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)',
|
color: 'rgb(75, 192, 192)',
|
||||||
fillColor: {
|
fillColor: {
|
||||||
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
||||||
@@ -210,6 +253,9 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
type: 'area',
|
type: 'area',
|
||||||
|
connectNulls: false,
|
||||||
|
gapSize: 2 * 24 * 3600 * 1000,
|
||||||
|
gapUnit: 'value',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueSuffix: ' hPa'
|
valueSuffix: ' hPa'
|
||||||
}
|
}
|
||||||
@@ -227,7 +273,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
yAxisTitle = 'Regen (mm) / Rate (mm/h)'
|
yAxisTitle = 'Regen (mm) / Rate (mm/h)'
|
||||||
series = [{
|
series = [{
|
||||||
name: 'Regen',
|
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)',
|
color: 'rgb(54, 162, 235)',
|
||||||
fillColor: 'rgba(54, 162, 235, 0.3)',
|
fillColor: 'rgba(54, 162, 235, 0.3)',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
@@ -236,7 +282,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
name: 'Regenrate',
|
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)',
|
color: 'rgb(59, 130, 246)',
|
||||||
dashStyle: 'Dash',
|
dashStyle: 'Dash',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -249,7 +295,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
yAxisTitle = 'Regen (mm pro Tag)'
|
yAxisTitle = 'Regen (mm pro Tag)'
|
||||||
series = [{
|
series = [{
|
||||||
name: 'Regen',
|
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)',
|
color: 'rgb(54, 162, 235)',
|
||||||
type: 'column',
|
type: 'column',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -261,7 +307,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
yAxisTitle = 'Regen (mm pro Woche)'
|
yAxisTitle = 'Regen (mm pro Woche)'
|
||||||
series = [{
|
series = [{
|
||||||
name: 'Regen',
|
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)',
|
color: 'rgb(54, 162, 235)',
|
||||||
type: 'column',
|
type: 'column',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -281,48 +327,79 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
}, [sortedData, rainData, timeRange])
|
}, [sortedData, rainData, timeRange])
|
||||||
|
|
||||||
// Windgeschwindigkeit Chart
|
// Windgeschwindigkeit Chart
|
||||||
const windSpeedOptions = useMemo(() => ({
|
const windSpeedOptions = useMemo(() => {
|
||||||
...getCommonOptions(),
|
// Bei 365d nur Windgeschwindigkeit, keine Böen
|
||||||
plotOptions: {
|
const series = timeRange === '365d'
|
||||||
series: {
|
? [{
|
||||||
marker: {
|
name: 'Windgeschwindigkeit',
|
||||||
enabled: false
|
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: {
|
||||||
|
marker: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
lineWidth: 2
|
||||||
},
|
},
|
||||||
lineWidth: 2
|
line: {
|
||||||
},
|
step: 'left' // Keine Glättung
|
||||||
line: {
|
|
||||||
step: 'left' // Keine Glättung
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...getCommonOptions().yAxis,
|
|
||||||
title: {
|
|
||||||
text: 'Windspeed (km/h)',
|
|
||||||
style: {
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
yAxis: {
|
||||||
series: [{
|
...getCommonOptions().yAxis,
|
||||||
name: 'Windgeschwindigkeit',
|
title: {
|
||||||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
|
text: 'Windspeed (km/h)',
|
||||||
color: 'rgb(153, 102, 255)',
|
style: {
|
||||||
fillColor: 'rgba(153, 102, 255, 0.1)',
|
whiteSpace: 'nowrap'
|
||||||
type: 'area',
|
}
|
||||||
tooltip: {
|
}
|
||||||
valueSuffix: ' km/h'
|
},
|
||||||
}
|
series
|
||||||
}, {
|
}
|
||||||
name: 'Windböen',
|
}, [sortedData, timeRange])
|
||||||
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])
|
|
||||||
|
|
||||||
// Windrichtung Chart
|
// Windrichtung Chart
|
||||||
const windDirOptions = useMemo(() => ({
|
const windDirOptions = useMemo(() => ({
|
||||||
@@ -359,7 +436,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Windrichtung',
|
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)',
|
color: 'rgb(54, 162, 235)',
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -465,7 +542,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
{/* Charts Grid */}
|
{/* Charts Grid */}
|
||||||
<div className="charts-grid">
|
<div className="charts-grid">
|
||||||
<div className="chart-container">
|
<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">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
||||||
</div>
|
</div>
|
||||||
@@ -475,7 +552,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chart-container">
|
<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">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
|
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +562,7 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chart-container">
|
<div className="chart-container">
|
||||||
<h3>💧 Luftfeuchtigkeit - Aktuell: {current.humidity || '-'}%</h3>
|
<h3>💧 Luftfeuchtigkeit{aggregationSuffix} - Aktuell: {current.humidity || '-'}%</h3>
|
||||||
<div className="chart-wrapper">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
|
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
|
||||||
</div>
|
</div>
|
||||||
@@ -495,21 +572,21 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chart-container">
|
<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">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
|
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chart-container">
|
<div className="chart-container">
|
||||||
<h3>🧭 Windrichtung - Aktuell: {current.wind_dir ?? '-'}°</h3>
|
<h3>🧭 Windrichtung{aggregationSuffix} - Aktuell: {current.wind_dir ?? '-'}°</h3>
|
||||||
<div className="chart-wrapper">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={windDirOptions} />
|
<HighchartsReact highcharts={Highcharts} options={windDirOptions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chart-container">
|
<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">
|
<div className="chart-wrapper">
|
||||||
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
|
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
219
migrate_sqlite_to_postgres.py
Executable file
219
migrate_sqlite_to_postgres.py
Executable 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()
|
||||||
Reference in New Issue
Block a user