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()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
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,))
|
||||
# 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,
|
||||
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()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
@@ -440,15 +460,26 @@ async def get_weekly_rain_data(
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
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,))
|
||||
# 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,
|
||||
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()
|
||||
|
||||
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 {
|
||||
width: 100%;
|
||||
max-width: 1900px;
|
||||
/* max-width: 1900px; */
|
||||
max-width: 795px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,48 +327,79 @@ const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeC
|
||||
}, [sortedData, rainData, timeRange])
|
||||
|
||||
// Windgeschwindigkeit Chart
|
||||
const windSpeedOptions = useMemo(() => ({
|
||||
...getCommonOptions(),
|
||||
plotOptions: {
|
||||
series: {
|
||||
marker: {
|
||||
enabled: false
|
||||
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: {
|
||||
marker: {
|
||||
enabled: false
|
||||
},
|
||||
lineWidth: 2
|
||||
},
|
||||
lineWidth: 2
|
||||
},
|
||||
line: {
|
||||
step: 'left' // Keine Glättung
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
...getCommonOptions().yAxis,
|
||||
title: {
|
||||
text: 'Windspeed (km/h)',
|
||||
style: {
|
||||
whiteSpace: 'nowrap'
|
||||
line: {
|
||||
step: 'left' // Keine Glättung
|
||||
}
|
||||
}
|
||||
},
|
||||
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'
|
||||
}
|
||||
}, {
|
||||
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])
|
||||
},
|
||||
yAxis: {
|
||||
...getCommonOptions().yAxis,
|
||||
title: {
|
||||
text: 'Windspeed (km/h)',
|
||||
style: {
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
}
|
||||
},
|
||||
series
|
||||
}
|
||||
}, [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
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