Compare commits

...

7 Commits

Author SHA1 Message Date
rxf
0285fde580 Zeiten besser dargestellt
Werte in europäisches System umgerechnet
2026-01-28 14:18:00 +00:00
rxf
511cc31dc0 Einlesen und Anzeigen getrennt
Einlesen per HTPP (nicht mehr MQTT)
2026-01-27 12:52:54 +00:00
rxf
c3614ebab0 erster test mit http 2026-01-26 16:38:50 +01:00
rxf
c6c3f4cf37 Tools zum Einlesen dazu in tools directory 2026-01-26 16:22:06 +01:00
rxf
cff7f80463 Docker draus gemacht 2026-01-25 19:48:00 +01:00
rxf
5f516f5dd4 Trennung HTML/Javascript von Python
Wind als Linechart
2026-01-25 10:53:47 +01:00
rxf
2907f5de18 Versuche mit HighCharts 2026-01-24 20:42:04 +01:00
14 changed files with 1241 additions and 489 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# MQTT Broker Konfiguration
MQTT_HOST=rexfue.de
MQTT_PORT=1883
MQTT_TOPIC=vantage/live
MQTT_USER=your_mqtt_username
MQTT_PASSWORD=your_mqtt_password
# Datenbank
DB_FILE=wetterdaten.db

1
.gitignore vendored
View File

@@ -43,6 +43,7 @@ env/
# Database # Database
wetterdaten.db wetterdaten.db
*.db *.db
*.sdb
# Logs # Logs
*.log *.log

129
DOCKER_README.md Normal file
View File

@@ -0,0 +1,129 @@
# Wetterstation Docker Setup
## Architektur
Die Wetterstation besteht aus zwei unabhängigen Services:
1. **Ingestion Service** (Port 5004): Empfängt POST-Daten und schreibt in die Datenbank
2. **Web Service** (Port 5003): Stellt das Web-Interface und Lese-APIs bereit
Beide Services teilen sich eine gemeinsame SQLite-Datenbank via Volume.
## Voraussetzungen
- Docker und Docker Compose installiert
## Installation
### 1. `.env` Datei erstellen (optional)
Erstelle eine `.env` Datei für benutzerdefinierte Konfiguration:
```bash
DB_FILE=wetterdaten.db
HTTP_PORT=5003
INGESTION_PORT=5004
```
### 2. Container starten
```bash
docker-compose up -d
```
Die Services laufen dann unter:
- Web-Interface: `http://localhost:5003`
- Ingestion API: `http://localhost:5004/api/data/upload`
### 3. Container verwalten
```bash
# Logs anschauen (beide Services)
docker-compose logs -f
# Logs nur Ingestion Service
docker-compose logs -f wetterstation-ingestion
# Logs nur Web Service
docker-compose logs -f wetterstation-web
# Container stoppen
docker-compose down
# Container neustarten
docker-compose restart
# Nur Ingestion Service neustarten
docker-compose restart wetterstation-ingestion
```
## Datenverwaltung
Die SQLite-Datenbank (`wetterdaten.db`) wird als Volume persistiert und bleibt erhalten, auch wenn die Container gelöscht werden.
## Services im Detail
### Ingestion Service
- **Port**: 5004
- **Endpoints**:
- `POST /api/data/upload` - Empfängt Wetterdaten
- `GET /health` - Health-Check
- **Zweck**: Schreibt Daten in die Datenbank
### Web Service
- **Port**: 5003
- **Endpoints**:
- `GET /` - Web-Interface
- `GET /api/data/day` - Daten der letzten 24h
- `GET /api/data/week` - Daten der letzten Woche
- `GET /health` - Health-Check
- **Zweck**: Visualisierung und Datenabruf
## Externe Zugriffe
### Daten senden (von außen)
```bash
curl -X POST http://your-server-ip:5004/api/data/upload \
-H 'Content-Type: application/json' \
-d '{
"dateTime": "2026-01-27 12:00:00",
"barometer": 1013.2,
"outTemp": 5.6,
"outHumidity": 72,
"windSpeed": 3.2,
"windDir": 180,
"windGust": 5.0,
"rainRate": 0.0,
"rain": 0.0
}'
```
### Web-Interface aufrufen
Öffne im Browser: `http://your-server-ip:5003`
## Troubleshooting
### Datenbank-Fehler
Falls die Datenbank beschädigt ist:
```bash
rm wetterdaten.db
docker-compose restart wetterstation-ingestion
```
### Container neu bauen
Nach Code-Änderungen:
```bash
docker-compose build --no-cache
docker-compose up -d
```
### Port-Konflikte
Falls Ports bereits belegt sind, passe die Ports in `docker-compose.yml` an:
```yaml
ports:
- "NEUER_PORT:5003" # für Web Service
- "NEUER_PORT:5004" # für Ingestion Service
```
### Health-Checks
Überprüfe, ob Services laufen:
```bash
curl http://localhost:5003/health
curl http://localhost:5004/health
```

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Multi-stage build: Leichtgewichtiger Container
FROM python:3.13-slim
# Setze Arbeitsverzeichnis
WORKDIR /app
# Installiere Dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Kopiere die Anwendung
COPY wetterstation.py .
COPY static/ static/
COPY templates/ templates/
# Exponiere Port
EXPOSE 5003
# Starten Sie die Anwendung
CMD ["python", "wetterstation.py"]

18
Dockerfile.ingestion Normal file
View File

@@ -0,0 +1,18 @@
# Multi-stage build: Leichtgewichtiger Container für Ingestion Service
FROM python:3.13-slim
# Setze Arbeitsverzeichnis
WORKDIR /app
# Installiere Dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Kopiere die Anwendung
COPY wetterstation_ingestion.py .
# Exponiere Port
EXPOSE 5004
# Starten Sie die Anwendung
CMD ["python", "wetterstation_ingestion.py"]

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
services:
# Ingestion Service - empfängt POST-Daten
wetterstation-ingestion:
build:
context: .
dockerfile: Dockerfile.ingestion
container_name: wetterstation-ingestion
ports:
- "5004:5004"
volumes:
- ./wetterdaten.db:/app/wetterdaten.db
env_file:
- .env
environment:
- FLASK_ENV=production
- INGESTION_PORT=5004
restart: unless-stopped
networks:
- wetterstation_network
# Web Service - stellt UI und Lese-APIs bereit
wetterstation-web:
build:
context: .
dockerfile: Dockerfile
container_name: wetterstation-web
ports:
- "5003:5003"
volumes:
- ./wetterdaten.db:/app/wetterdaten.db
env_file:
- .env
environment:
- FLASK_ENV=production
restart: unless-stopped
networks:
- wetterstation_network
depends_on:
- wetterstation-ingestion
networks:
wetterstation_network:
driver: bridge

View File

@@ -1,2 +1,2 @@
flask==3.0.0 flask==3.0.0
paho-mqtt==1.6.1 python-dotenv==1.0.0

89
static/css/style.css Normal file
View File

@@ -0,0 +1,89 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.tabs {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.tab {
padding: 15px 40px;
background: #f0f0f0;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
font-weight: 600;
}
.tab:hover {
background: #e0e0e0;
transform: translateY(-2px);
}
.tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
margin-top: 20px;
}
.chart-container {
background: #f9f9f9;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
/* feste Mindesthöhe sorgt für konsistente Layouts */
min-height: 420px;
}
/* Stellt sicher, dass Charts nicht breiter als ihre Container werden */
#charts { width: 100%; }
.chart-container > div { width: 100%; max-width: 100%; }
.loading {
text-align: center;
padding: 50px;
font-size: 20px;
color: #666;
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: 1fr;
}
}

225
static/js/app.js Normal file
View File

@@ -0,0 +1,225 @@
let currentPeriod = 'day';
const DEFAULT_CHART_HEIGHT = 360;
const POLAR_CHART_HEIGHT = 420;
// HighCharts globale Einstellungen
Highcharts.setOptions({
lang: {
months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
shortMonths: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
weekdays: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
shortWeekdays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
}
});
function switchPeriod(period) {
currentPeriod = period;
// Tab-Status aktualisieren
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
loadData();
}
function loadData() {
document.getElementById('loading').style.display = 'block';
document.getElementById('charts').style.display = 'none';
fetch(`/api/data/${currentPeriod}`)
.then(response => response.json())
.then(data => {
renderCharts(data);
document.getElementById('loading').style.display = 'none';
document.getElementById('charts').style.display = 'grid';
})
.catch(error => {
console.error('Fehler beim Laden:', error);
document.getElementById('loading').innerHTML = 'Fehler beim Laden der Daten';
});
}
function renderCharts(apiData) {
const data = apiData.data;
const rainData = apiData.rain_hourly;
// Konvertiere Timestamps in Millisekunden
const timestamps = data.map(d => d.dateTime)
const rainTimestamps = rainData.map(d => new Date(d.hour).getTime())
// Berechne Zeitbereich für die Achsen
const minTime = Math.min(...timestamps, ...rainTimestamps);
const maxTime = Math.max(...timestamps, ...rainTimestamps);
// 1-Stunden-Intervalle für die Achsen-Labels (3600000 ms = 1 Stunde)
const ONE_HOUR = 3600000;
const FOUR_HOURS = ONE_HOUR * 4;
// Temperatur (Fahrenheit -> Celsius umrechnen)
Highcharts.chart('temp-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '🌡️ Temperatur (°C)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: { title: { text: '°C' } },
legend: { enabled: true },
series: [{
name: 'Temperatur',
data: data.map((d, i) => [d.dateTime*1000, (d.outTemp - 32) * 5/9]),
color: '#ff6b6b',
lineWidth: 2
}],
credits: { enabled: false }
});
// Luftfeuchtigkeit
Highcharts.chart('humidity-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '💧 Luftfeuchtigkeit (%)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: { title: { text: '%' } },
legend: { enabled: true },
series: [{
name: 'Luftfeuchtigkeit',
data: data.map((d, i) => [d.dateTime*1000, d.outHumidity]),
color: '#4ecdc4',
lineWidth: 2
}],
credits: { enabled: false }
});
// Luftdruck (inHg -> hPa umrechnen)
Highcharts.chart('pressure-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '🎈 Luftdruck (hPa)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: { title: { text: 'hPa' } },
legend: { enabled: true },
series: [{
name: 'Luftdruck',
data: data.map((d, i) => [d.dateTime*1000, d.barometer * 33.8639]),
color: '#95e1d3',
lineWidth: 2
}],
credits: { enabled: false }
});
// Regenmenge pro Stunde
Highcharts.chart('rain-chart', {
chart: { type: 'column', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '🌧️ Regenmenge pro Stunde (mm)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: { title: { text: 'mm' } },
legend: { enabled: false },
series: [{
name: 'Regen',
data: rainData.map((d, i) => [d.dateTime*1000, d.rain]),
color: '#3498db'
}],
credits: { enabled: false }
});
// Windgeschwindigkeit (mph -> km/h umrechnen)
Highcharts.chart('wind-speed-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '💨 Windgeschwindigkeit (km/h)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: { title: { text: 'km/h' } },
legend: { enabled: true },
series: [{
name: 'Windgeschwindigkeit',
data: data.map((d, i) => [d.dateTime*1000, d.windSpeed * 1.60934]),
color: 'blue',
lineWidth: 2
}, {
name: 'Böen',
data: data.map((d, i) => [d.dateTime*1000, d.windGust * 1.60934]),
color: 'red',
lineWidth: 2,
}],
credits: { enabled: false }
});
// Windrichtung
Highcharts.chart('wind-dir-chart', {
chart: { type: 'scatter', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '🧭 Windrichtung (°)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS,
gridLineWidth: 1
},
yAxis: {
title: { text: 'Richtung (°)' },
min: 0,
max: 360,
// tickPositions: [0, 90, 180, 270, 360]
tickInterval: 90,
labels: {
formatter: function() {
// Windrichtungen zuordnen
const directions = {
0: 'Nord',
90: 'Ost',
180: 'Süd',
270: 'West',
360: 'Nord'
};
return directions[this.value] || this.value + '°';
}
}
},
legend: { enabled: true },
series: [{
name: 'Windrichtung',
data: data.map((d, i) => [d.dateTime*1000, d.windDir || 0]),
color: '#f39c12',
marker: {
radius: 2
}
// lineWidth: 2
}],
credits: { enabled: false }
});
}
// Initiales Laden
loadData();
// Auto-Refresh alle 5 Minuten
setInterval(loadData, 5 * 60 * 1000);

191
tools/sqlite_copy.py Normal file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
SQLite Datenkopier-Programm
Kopiert ausgewählte Spalten von einer SQLite-Datenbank in eine andere
"""
import sqlite3
import sys
from pathlib import Path
from datetime import datetime
# Zu kopierende Spalten
COLUMNS = [
'dateTime',
'barometer',
'outTemp',
'outHumidity',
'windSpeed',
'windDir',
'windGust',
'rainRate',
'rain'
]
def format_datetime(timestamp):
"""Konvertiert Unix-Timestamp in lesbares Format"""
try:
if timestamp is None:
return 'NULL'
return datetime.fromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, OSError):
return str(timestamp)
def get_table_name(cursor):
"""Ermittelt den Namen der Quelltabelle"""
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
tables = cursor.fetchall()
if not tables:
print("Fehler: Keine Tabellen in der Quelldatenbank gefunden!")
sys.exit(1)
if len(tables) == 1:
return tables[0][0]
print("\nVerfügbare Tabellen:")
for i, (table,) in enumerate(tables, 1):
print(f"{i}. {table}")
while True:
try:
choice = int(input("\nWählen Sie die Nummer der Quelltabelle: "))
if 1 <= choice <= len(tables):
return tables[choice - 1][0]
except (ValueError, IndexError):
pass
print("Ungültige Eingabe. Bitte erneut versuchen.")
def check_columns(cursor, table_name, columns):
"""Prüft, welche Spalten in der Tabelle existieren"""
cursor.execute(f"PRAGMA table_info({table_name})")
existing_cols = {row[1] for row in cursor.fetchall()}
available = [col for col in columns if col in existing_cols]
missing = [col for col in columns if col not in existing_cols]
return available, missing
def copy_data(source_db, target_db, source_table, target_table, columns):
"""Kopiert die Daten von der Quell- zur Zieldatenbank"""
# Verbindungen herstellen
conn_source = sqlite3.connect(source_db)
conn_target = sqlite3.connect(target_db)
try:
cursor_source = conn_source.cursor()
cursor_target = conn_target.cursor()
# Quelltabelle ermitteln
if source_table is None:
source_table = get_table_name(cursor_source)
print(f"\nQuelltabelle: {source_table}")
# Verfügbare Spalten prüfen
available_cols, missing_cols = check_columns(cursor_source, source_table, columns)
if missing_cols:
print(f"\nWarnung: Folgende Spalten existieren nicht in der Quelltabelle: {', '.join(missing_cols)}")
if not available_cols:
print("Fehler: Keine der gewünschten Spalten gefunden!")
return False
print(f"Zu kopierende Spalten: {', '.join(available_cols)}")
# Zieltabelle erstellen
columns_def = ', '.join([f"{col} REAL" if col != 'dateTime' else f"{col} INTEGER PRIMARY KEY"
for col in available_cols])
cursor_target.execute(f"CREATE TABLE IF NOT EXISTS {target_table} ({columns_def})")
# Daten zählen
cursor_source.execute(f"SELECT COUNT(*) FROM {source_table}")
total_rows = cursor_source.fetchone()[0]
print(f"\nGesamtanzahl Datensätze: {total_rows}")
# Vorschau der ersten Zeile
cursor_source.execute(f"SELECT {columns_str} FROM {source_table} LIMIT 1")
first_row = cursor_source.fetchone()
if first_row:
print("\nVorschau erste Zeile:")
for i, col in enumerate(available_cols):
value = first_row[i]
if col == 'dateTime':
print(f" {col}: {format_datetime(value)} (Timestamp: {value})")
else:
print(f" {col}: {value}")
# Daten kopieren
columns_str = ', '.join(available_cols)
placeholders = ', '.join(['?' for _ in available_cols])
cursor_source.execute(f"SELECT {columns_str} FROM {source_table}")
batch_size = 1000
copied = 0
while True:
rows = cursor_source.fetchmany(batch_size)
if not rows:
break
cursor_target.executemany(
f"INSERT OR REPLACE INTO {target_table} ({columns_str}) VALUES ({placeholders})",
rows
)
copied += len(rows)
print(f"Kopiert: {copied}/{total_rows} Datensätze...", end='\r')
conn_target.commit()
print(f"\n\n✓ Erfolgreich {copied} Datensätze kopiert!")
return True
except sqlite3.Error as e:
print(f"\nFehler beim Kopieren: {e}")
return False
finally:
conn_source.close()
conn_target.close()
def main():
print("=" * 60)
print("SQLite Datenkopier-Programm")
print("=" * 60)
# Eingaben
if len(sys.argv) >= 3:
source_db = sys.argv[1]
target_db = sys.argv[2]
source_table = sys.argv[3] if len(sys.argv) >= 4 else None
target_table = sys.argv[4] if len(sys.argv) >= 5 else 'weather_data'
else:
source_db = input("\nPfad zur Quelldatenbank: ").strip()
target_db = input("Pfad zur Zieldatenbank: ").strip()
source_table = input("Name der Quelltabelle (leer=automatisch ermitteln): ").strip() or None
target_table = input("Name der Zieltabelle [weather_data]: ").strip() or 'weather_data'
# Validierung
if not Path(source_db).exists():
print(f"\nFehler: Quelldatenbank '{source_db}' nicht gefunden!")
sys.exit(1)
# Kopieren
success = copy_data(source_db, target_db, source_table, target_table, COLUMNS)
if success:
print(f"\nZieldatenbank: {target_db}")
print(f"Zieltabelle: {target_table}")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

168
tools/sqlite_copy_1.py Normal file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
SQLite Datenkopier-Programm
Kopiert ausgewählte Spalten von einer SQLite-Datenbank in eine andere
"""
import sqlite3
import sys
from pathlib import Path
# Zu kopierende Spalten
COLUMNS = [
'dateTime',
'barometer',
'outTemp',
'outHumidity',
'windSpeed',
'windDir',
'windGust',
'rainRate',
'rain'
]
def get_table_name(cursor):
"""Ermittelt den Namen der Quelltabelle"""
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
tables = cursor.fetchall()
if not tables:
print("Fehler: Keine Tabellen in der Quelldatenbank gefunden!")
sys.exit(1)
if len(tables) == 1:
return tables[0][0]
print("\nVerfügbare Tabellen:")
for i, (table,) in enumerate(tables, 1):
print(f"{i}. {table}")
while True:
try:
choice = int(input("\nWählen Sie die Nummer der Quelltabelle: "))
if 1 <= choice <= len(tables):
return tables[choice - 1][0]
except (ValueError, IndexError):
pass
print("Ungültige Eingabe. Bitte erneut versuchen.")
def check_columns(cursor, table_name, columns):
"""Prüft, welche Spalten in der Tabelle existieren"""
cursor.execute(f"PRAGMA table_info({table_name})")
existing_cols = {row[1] for row in cursor.fetchall()}
available = [col for col in columns if col in existing_cols]
missing = [col for col in columns if col not in existing_cols]
return available, missing
def copy_data(source_db, target_db, source_table, target_table, columns):
"""Kopiert die Daten von der Quell- zur Zieldatenbank"""
# Verbindungen herstellen
conn_source = sqlite3.connect(source_db)
conn_target = sqlite3.connect(target_db)
try:
cursor_source = conn_source.cursor()
cursor_target = conn_target.cursor()
# Quelltabelle ermitteln
if source_table is None:
source_table = get_table_name(cursor_source)
print(f"\nQuelltabelle: {source_table}")
# Verfügbare Spalten prüfen
available_cols, missing_cols = check_columns(cursor_source, source_table, columns)
if missing_cols:
print(f"\nWarnung: Folgende Spalten existieren nicht in der Quelltabelle: {', '.join(missing_cols)}")
if not available_cols:
print("Fehler: Keine der gewünschten Spalten gefunden!")
return False
print(f"Zu kopierende Spalten: {', '.join(available_cols)}")
# Zieltabelle erstellen
columns_def = ', '.join([f"{col} REAL" if col != 'dateTime' else f"{col} INTEGER PRIMARY KEY"
for col in available_cols])
cursor_target.execute(f"CREATE TABLE IF NOT EXISTS {target_table} ({columns_def})")
# Daten zählen
cursor_source.execute(f"SELECT COUNT(*) FROM {source_table}")
total_rows = cursor_source.fetchone()[0]
print(f"\nGesamtanzahl Datensätze: {total_rows}")
# Daten kopieren
columns_str = ', '.join(available_cols)
placeholders = ', '.join(['?' for _ in available_cols])
cursor_source.execute(f"SELECT {columns_str} FROM {source_table}")
batch_size = 1000
copied = 0
while True:
rows = cursor_source.fetchmany(batch_size)
if not rows:
break
cursor_target.executemany(
f"INSERT OR REPLACE INTO {target_table} ({columns_str}) VALUES ({placeholders})",
rows
)
copied += len(rows)
print(f"Kopiert: {copied}/{total_rows} Datensätze...", end='\r')
conn_target.commit()
print(f"\n\n✓ Erfolgreich {copied} Datensätze kopiert!")
return True
except sqlite3.Error as e:
print(f"\nFehler beim Kopieren: {e}")
return False
finally:
conn_source.close()
conn_target.close()
def main():
print("=" * 60)
print("SQLite Datenkopier-Programm")
print("=" * 60)
# Eingaben
if len(sys.argv) >= 3:
source_db = sys.argv[1]
target_db = sys.argv[2]
source_table = sys.argv[3] if len(sys.argv) >= 4 else None
target_table = sys.argv[4] if len(sys.argv) >= 5 else 'weather_data'
else:
source_db = input("\nPfad zur Quelldatenbank: ").strip()
target_db = input("Pfad zur Zieldatenbank: ").strip()
source_table = input("Name der Quelltabelle (leer=automatisch ermitteln): ").strip() or None
target_table = input("Name der Zieltabelle [weather_data]: ").strip() or 'weather_data'
# Validierung
if not Path(source_db).exists():
print(f"\nFehler: Quelldatenbank '{source_db}' nicht gefunden!")
sys.exit(1)
# Kopieren
success = copy_data(source_db, target_db, source_table, target_table, COLUMNS)
if success:
print(f"\nZieldatenbank: {target_db}")
print(f"Zieltabelle: {target_table}")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

193
tools/sqlite_query.py Normal file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
SQLite Query Tool mit formatierter DateTime-Ausgabe
Zeigt dateTime als 'YYYY-MM-DD HH:MM:SS' statt Unix-Timestamp
"""
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
def format_datetime(timestamp):
"""Konvertiert Unix-Timestamp in lesbares Format"""
try:
if timestamp is None:
return 'NULL'
return datetime.fromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, OSError):
return str(timestamp)
def format_value(value):
"""Formatiert Werte für die Ausgabe"""
if value is None:
return 'NULL'
if isinstance(value, float):
return f'{value:.2f}'
return str(value)
def execute_query(db_path, query=None, limit=None):
"""Führt eine SELECT-Abfrage aus und zeigt Ergebnisse formatiert an"""
if not Path(db_path).exists():
print(f"Fehler: Datenbank '{db_path}' nicht gefunden!")
return False
conn = sqlite3.connect(db_path)
try:
cursor = conn.cursor()
# Wenn keine Query angegeben, zeige verfügbare Tabellen
if not query:
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
tables = cursor.fetchall()
if not tables:
print("Keine Tabellen gefunden!")
return False
print("\nVerfügbare Tabellen:")
for table, in tables:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f" - {table} ({count} Datensätze)")
# Erste Tabelle als Standard verwenden
default_table = tables[0][0]
query = f"SELECT * FROM {default_table}"
if limit:
query += f" LIMIT {limit}"
else:
query += " LIMIT 10"
print(f"\nStandard-Abfrage: {query}\n")
# Query ausführen
cursor.execute(query)
# Spaltennamen holen
columns = [description[0] for description in cursor.description]
# Prüfen, ob dateTime-Spalte vorhanden ist
datetime_index = None
if 'dateTime' in columns:
datetime_index = columns.index('dateTime')
# Daten abrufen
rows = cursor.fetchall()
if not rows:
print("Keine Ergebnisse gefunden.")
return True
print(f"Anzahl Ergebnisse: {len(rows)}\n")
# Spaltenbreiten berechnen
col_widths = []
for i, col in enumerate(columns):
if i == datetime_index:
max_width = max(len(col), 19) # 'YYYY-MM-DD HH:MM:SS' = 19 Zeichen
else:
max_width = len(col)
for row in rows[:100]: # Nur erste 100 Zeilen für Breitenberechnung
max_width = max(max_width, len(format_value(row[i])))
col_widths.append(min(max_width, 30)) # Max 30 Zeichen pro Spalte
# Header ausgeben
header = ' | '.join(col.ljust(col_widths[i]) for i, col in enumerate(columns))
print(header)
print('-' * len(header))
# Daten ausgeben
for row in rows:
formatted_row = []
for i, value in enumerate(row):
if i == datetime_index:
formatted_row.append(format_datetime(value).ljust(col_widths[i]))
else:
formatted_row.append(format_value(value).ljust(col_widths[i]))
print(' | '.join(formatted_row))
return True
except sqlite3.Error as e:
print(f"Datenbankfehler: {e}")
return False
finally:
conn.close()
def interactive_mode(db_path):
"""Interaktiver Modus für mehrere Abfragen"""
print("=" * 70)
print("SQLite Query Tool - Interaktiver Modus")
print("=" * 70)
print("\nTipps:")
print(" - Geben Sie SQL-Abfragen direkt ein")
print(" - Drücken Sie Enter ohne Eingabe für Standard-Abfrage (erste 10 Zeilen)")
print(" - Geben Sie 'exit' oder 'quit' zum Beenden ein")
print(" - dateTime wird automatisch als 'YYYY-MM-DD HH:MM:SS' angezeigt\n")
execute_query(db_path) # Zeige verfügbare Tabellen und Standard-Abfrage
while True:
try:
query = input("\nSQL> ").strip()
if query.lower() in ('exit', 'quit', 'q'):
print("Auf Wiedersehen!")
break
if not query:
# Standard-Abfrage
execute_query(db_path)
else:
execute_query(db_path, query)
except KeyboardInterrupt:
print("\n\nUnterbrochen. Auf Wiedersehen!")
break
except EOFError:
print("\nAuf Wiedersehen!")
break
def main():
if len(sys.argv) < 2:
print("Verwendung:")
print(" python sqlite_query.py <datenbank.db> # Interaktiver Modus")
print(" python sqlite_query.py <datenbank.db> '<SQL-Query>' # Einzelne Abfrage")
print(" python sqlite_query.py <datenbank.db> <tabelle> [limit] # Tabelle anzeigen")
print("\nBeispiele:")
print(" python sqlite_query.py wetter.db")
print(" python sqlite_query.py wetter.db 'SELECT * FROM weather_data WHERE outTemp > 20'")
print(" python sqlite_query.py wetter.db weather_data 50")
sys.exit(1)
db_path = sys.argv[1]
if len(sys.argv) == 2:
# Interaktiver Modus
interactive_mode(db_path)
elif len(sys.argv) == 3:
# Einzelne Query oder Tabellenname
arg = sys.argv[2]
if arg.upper().startswith('SELECT') or arg.upper().startswith('WITH'):
execute_query(db_path, arg)
else:
# Als Tabellenname interpretieren
execute_query(db_path, f"SELECT * FROM {arg} LIMIT 10")
elif len(sys.argv) == 4:
# Tabelle mit Limit
table = sys.argv[2]
limit = sys.argv[3]
execute_query(db_path, f"SELECT * FROM {table} LIMIT {limit}")
if __name__ == "__main__":
main()

View File

@@ -1,81 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Wetterstation - MQTT Datenempfang und Web-Visualisierung Wetterstation Web-Interface - Visualisierung und API
Stellt das Web-Interface und Lese-APIs für historische Daten bereit
""" """
import sqlite3 import sqlite3
import json import json
import threading import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import Flask, render_template, jsonify from flask import Flask, render_template, jsonify
import paho.mqtt.client as mqtt from dotenv import load_dotenv
# Konfiguration # Lade Umgebungsvariablen aus .env Datei
MQTT_HOST = "rexfue.de" load_dotenv()
MQTT_PORT = 1883
MQTT_TOPIC = "vantage/live" # Bitte anpassen!
MQTT_USER = "stzuhr" # Bitte anpassen!
MQTT_PASSWORD = "74chQCYb" # Bitte anpassen!
DB_FILE = "wetterdaten.db" # Konfiguration aus Umgebungsvariablen
DB_FILE = os.getenv("DB_FILE", "wetterdaten.db")
HTTP_PORT = int(os.getenv("HTTP_PORT", 5003))
app = Flask(__name__) app = Flask(__name__)
class WetterDB: class WetterDB:
"""Klasse für Datenbankoperationen""" """Klasse für Datenbankoperationen (nur Lesezugriff)"""
def __init__(self, db_file): def __init__(self, db_file):
self.db_file = db_file self.db_file = db_file
self.init_db()
def init_db(self):
"""Datenbank initialisieren"""
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS wetterdaten (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT NOT NULL,
pressure REAL,
wind_gust REAL,
wind_speed REAL,
wind_dir REAL,
rain_rate REAL,
rain REAL,
humidity INTEGER,
temperature REAL
)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_datetime ON wetterdaten(datetime)
''')
conn.commit()
conn.close()
def save_data(self, data):
"""Wetterdaten speichern"""
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO wetterdaten
(datetime, pressure, wind_gust, wind_speed, wind_dir,
rain_rate, rain, humidity, temperature)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data['datetime'],
data.get('pressure'),
data.get('wind_gust'),
data.get('wind_speed'),
data.get('wind_dir'),
data.get('rain_rate'),
data.get('rain'),
data.get('humidity'),
data.get('temperature')
))
conn.commit()
conn.close()
print(f"Daten gespeichert: {data['datetime']}")
def get_data(self, hours=24): def get_data(self, hours=24):
"""Daten der letzten X Stunden abrufen""" """Daten der letzten X Stunden abrufen"""
@@ -83,12 +33,12 @@ class WetterDB:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
time_threshold = (datetime.now() - timedelta(hours=hours)).strftime('%Y-%m-%d %H:%M:%S') time_threshold = int((datetime.now() - timedelta(hours=hours)).timestamp())
cursor.execute(''' cursor.execute('''
SELECT * FROM wetterdaten SELECT * FROM wetterdaten
WHERE datetime >= ? WHERE dateTime >= ?
ORDER BY datetime ASC ORDER BY dateTime ASC
''', (time_threshold,)) ''', (time_threshold,))
rows = cursor.fetchall() rows = cursor.fetchall()
@@ -101,14 +51,14 @@ class WetterDB:
conn = sqlite3.connect(self.db_file) conn = sqlite3.connect(self.db_file)
cursor = conn.cursor() cursor = conn.cursor()
time_threshold = (datetime.now() - timedelta(hours=hours)).strftime('%Y-%m-%d %H:%M:%S') time_threshold = int((datetime.now() - timedelta(hours=hours)).timestamp())
cursor.execute(''' cursor.execute('''
SELECT SELECT
strftime('%Y-%m-%d %H:00:00', datetime) as hour, strftime('%Y-%m-%d %H:00:00', datetime(dateTime, 'unixepoch', 'localtime')) as hour,
SUM(rain_rate) as total_rain SUM(rainRate) as total_rain
FROM wetterdaten FROM wetterdaten
WHERE datetime >= ? WHERE dateTime >= ?
GROUP BY hour GROUP BY hour
ORDER BY hour ASC ORDER BY hour ASC
''', (time_threshold,)) ''', (time_threshold,))
@@ -123,104 +73,6 @@ class WetterDB:
db = WetterDB(DB_FILE) db = WetterDB(DB_FILE)
class MQTTClient:
"""MQTT Client für Datenempfang"""
def __init__(self):
self.client = mqtt.Client()
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
# Stabilere Verbindungen bei Abbrüchen
try:
self.client.reconnect_delay_set(min_delay=1, max_delay=120)
except Exception:
pass
# Optionale Protokollierung (hilfreich beim Debuggen)
try:
self.client.enable_logger()
except Exception:
pass
self.client.on_connect = self.on_connect
self.client.on_message = self.on_message
@staticmethod
def _sanitize_data(payload: dict) -> dict:
"""Payload robust in erwartetes Format wandeln.
- Fehlende `datetime` wird mit aktueller Zeit ergänzt
- Felder in richtige Typen konvertieren
"""
def to_float(x):
try:
return float(x) if x is not None else None
except Exception:
return None
def to_int(x):
try:
return int(x) if x is not None else None
except Exception:
return None
# Zeitstempel: wenn vorhanden, in akzeptables Format bringen, sonst jetzt
dt = payload.get('datetime')
if isinstance(dt, (int, float)):
# Epoch -> ISO
dt = datetime.fromtimestamp(dt).strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(dt, str):
# Versuchen, ISO-Varianten in 'YYYY-MM-DD HH:MM:SS' zu überführen
# Entferne ggf. 'T' oder 'Z'
dt_clean = dt.replace('T', ' ').replace('Z', '').strip()
# Falls Millisekunden enthalten, abschneiden
if '.' in dt_clean:
dt_clean = dt_clean.split('.')[0]
# Bei zu kurzem String: fallback auf jetzt
if len(dt_clean) < 16:
dt = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
else:
dt = dt_clean
else:
dt = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
return {
'datetime': dt,
'pressure': to_float(payload.get('pressure')),
'wind_gust': to_float(payload.get('wind_gust')),
'wind_speed': to_float(payload.get('wind_speed')),
'wind_dir': to_float(payload.get('wind_dir')),
'rain_rate': to_float(payload.get('rain_rate')),
'rain': to_float(payload.get('rain')),
'humidity': to_int(payload.get('humidity')),
'temperature': to_float(payload.get('temperature')),
}
def on_connect(self, client, userdata, flags, rc):
"""Callback bei Verbindung"""
if rc == 0:
print(f"Mit MQTT Broker verbunden: {MQTT_HOST}")
client.subscribe(MQTT_TOPIC)
print(f"Topic abonniert: {MQTT_TOPIC}")
else:
print(f"Verbindungsfehler: {rc}")
def on_message(self, client, userdata, msg):
"""Callback bei empfangener Nachricht"""
try:
raw = json.loads(msg.payload.decode())
data = self._sanitize_data(raw)
print(f"Empfangen und gespeichert: {data}")
db.save_data(data)
except Exception as e:
print(f"Fehler beim Verarbeiten der Nachricht: {e}")
def start(self):
"""MQTT Client starten"""
try:
self.client.connect(MQTT_HOST, MQTT_PORT, 60)
self.client.loop_start()
print("MQTT Client gestartet")
except Exception as e:
print(f"MQTT Verbindungsfehler: {e}")
# Flask Routes # Flask Routes
@app.route('/') @app.route('/')
def index(): def index():
@@ -228,9 +80,15 @@ def index():
return render_template('index.html') return render_template('index.html')
@app.route('/health')
def health():
"""Health-Check Endpoint"""
return jsonify({'status': 'ok', 'service': 'web'}), 200
@app.route('/api/data/<period>') @app.route('/api/data/<period>')
def get_data(period): def get_historical_data(period):
"""API Endpoint für Wetterdaten""" """API Endpoint für historische Wetterdaten"""
hours = 24 if period == 'day' else 168 # 168h = 1 Woche hours = 24 if period == 'day' else 168 # 168h = 1 Woche
data = db.get_data(hours) data = db.get_data(hours)
rain_data = db.get_hourly_rain(hours) rain_data = db.get_hourly_rain(hours)
@@ -241,330 +99,16 @@ def get_data(period):
}) })
def create_html_template():
"""HTML Template erstellen"""
import os
os.makedirs('templates', exist_ok=True)
html_content = '''<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wetterstation</title>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.tabs {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.tab {
padding: 15px 40px;
background: #f0f0f0;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
font-weight: 600;
}
.tab:hover {
background: #e0e0e0;
transform: translateY(-2px);
}
.tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
margin-top: 20px;
}
.chart-container {
background: #f9f9f9;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
/* feste Mindesthöhe sorgt für konsistente Layouts */
min-height: 420px;
}
/* Stellt sicher, dass Charts nicht breiter als ihre Container werden */
#charts { width: 100%; }
.chart-container > div { width: 100%; max-width: 100%; }
.loading {
text-align: center;
padding: 50px;
font-size: 20px;
color: #666;
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🌤️ Wetterstation Dashboard</h1>
<div class="tabs">
<button class="tab active" onclick="switchPeriod('day')">Tag (24h)</button>
<button class="tab" onclick="switchPeriod('week')">Woche (7 Tage)</button>
</div>
<div class="loading" id="loading">Lade Daten...</div>
<div class="charts-grid" id="charts" style="display: none;">
<div class="chart-container">
<div id="temp-chart"></div>
</div>
<div class="chart-container">
<div id="humidity-chart"></div>
</div>
<div class="chart-container">
<div id="pressure-chart"></div>
</div>
<div class="chart-container">
<div id="rain-chart"></div>
</div>
<div class="chart-container">
<div id="wind-speed-chart"></div>
</div>
<div class="chart-container">
<div id="wind-dir-chart"></div>
</div>
</div>
</div>
<script>
let currentPeriod = 'day';
const DEFAULT_CHART_HEIGHT = 360;
const POLAR_CHART_HEIGHT = 420;
const plotConfig = { responsive: true, displayModeBar: false };
function switchPeriod(period) {
currentPeriod = period;
// Tab-Status aktualisieren
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
loadData();
}
function loadData() {
document.getElementById('loading').style.display = 'block';
document.getElementById('charts').style.display = 'none';
fetch(`/api/data/${currentPeriod}`)
.then(response => response.json())
.then(data => {
renderCharts(data);
document.getElementById('loading').style.display = 'none';
document.getElementById('charts').style.display = 'grid';
})
.catch(error => {
console.error('Fehler beim Laden:', error);
document.getElementById('loading').innerHTML = 'Fehler beim Laden der Daten';
});
}
function renderCharts(apiData) {
const data = apiData.data;
const rainData = apiData.rain_hourly;
const timestamps = data.map(d => d.datetime);
// Temperatur
Plotly.newPlot('temp-chart', [{
x: timestamps,
y: data.map(d => d.temperature),
type: 'scatter',
mode: 'lines',
name: 'Temperatur',
line: {color: '#ff6b6b', width: 3}
}], {
title: '🌡️ Temperatur (°C)',
xaxis: {title: 'Zeit'},
yaxis: {title: '°C'},
margin: {t: 50, b: 60, l: 60, r: 30, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: DEFAULT_CHART_HEIGHT
}, plotConfig);
// Luftfeuchtigkeit
Plotly.newPlot('humidity-chart', [{
x: timestamps,
y: data.map(d => d.humidity),
type: 'scatter',
mode: 'lines',
name: 'Luftfeuchtigkeit',
line: {color: '#4ecdc4', width: 3}
}], {
title: '💧 Luftfeuchtigkeit (%)',
xaxis: {title: 'Zeit'},
yaxis: {title: '%'},
margin: {t: 50, b: 60, l: 60, r: 30, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: DEFAULT_CHART_HEIGHT
}, plotConfig);
// Luftdruck
Plotly.newPlot('pressure-chart', [{
x: timestamps,
y: data.map(d => d.pressure),
type: 'scatter',
mode: 'lines',
name: 'Luftdruck',
line: {color: '#95e1d3', width: 3}
}], {
title: '🎈 Luftdruck (hPa)',
xaxis: {title: 'Zeit'},
yaxis: {title: 'hPa'},
margin: {t: 50, b: 60, l: 60, r: 30, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: DEFAULT_CHART_HEIGHT
}, plotConfig);
// Regenmenge pro Stunde
Plotly.newPlot('rain-chart', [{
x: rainData.map(d => d.hour),
y: rainData.map(d => d.rain),
type: 'bar',
name: 'Regen',
marker: {color: '#3498db'}
}], {
title: '🌧️ Regenmenge pro Stunde (mm)',
xaxis: {title: 'Zeit'},
yaxis: {title: 'mm'},
margin: {t: 50, b: 60, l: 60, r: 30, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: DEFAULT_CHART_HEIGHT
}, plotConfig);
// Windgeschwindigkeit
Plotly.newPlot('wind-speed-chart', [{
x: timestamps,
y: data.map(d => d.wind_speed),
type: 'scatter',
mode: 'lines',
name: 'Windgeschwindigkeit',
line: {color: '#f38181', width: 3}
}, {
x: timestamps,
y: data.map(d => d.wind_gust),
type: 'scatter',
mode: 'lines',
name: 'Böen',
line: {color: '#aa96da', width: 2, dash: 'dash'}
}], {
title: '💨 Windgeschwindigkeit (m/s)',
xaxis: {title: 'Zeit'},
yaxis: {title: 'm/s'},
margin: {t: 50, b: 60, l: 60, r: 30, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: DEFAULT_CHART_HEIGHT
}, plotConfig);
// Windrichtung (Polarplot)
Plotly.newPlot('wind-dir-chart', [{
r: data.map(d => d.wind_speed),
theta: data.map(d => d.wind_dir),
mode: 'markers',
type: 'scatterpolar',
marker: {
size: 8,
color: data.map(d => d.wind_speed),
colorscale: 'Viridis',
showscale: true,
colorbar: {title: 'm/s', orientation: 'h', y: -0.25}
}
}], {
title: '🧭 Windrichtung',
polar: {
radialaxis: {title: 'Geschwindigkeit (m/s)'},
angularaxis: {direction: 'clockwise'}
},
margin: {t: 50, b: 80, l: 60, r: 60, pad: 0},
legend: {orientation: 'h', y: -0.2},
height: POLAR_CHART_HEIGHT
}, plotConfig);
}
// Initiales Laden
loadData();
// Auto-Refresh alle 5 Minuten
setInterval(loadData, 5 * 60 * 1000);
</script>
</body>
</html>'''
with open('templates/index.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print("HTML Template erstellt: templates/index.html")
def main(): def main():
"""Hauptprogramm""" """Hauptprogramm"""
print("Wetterstation wird gestartet...") print("Wetterstation Web-Interface wird gestartet...")
print(f"\nWeb-Interface verfügbar unter: http://0.0.0.0:{HTTP_PORT}")
# HTML Template erstellen print(f"API Endpoints:")
create_html_template() print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/day")
print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/week")
# MQTT Client starten print(f"Health-Check: http://0.0.0.0:{HTTP_PORT}/health")
mqtt_client = MQTTClient()
mqtt_client.start()
# Flask Server starten
print("\nWeb-Interface verfügbar unter: http://localhost:5003")
print("Drücke CTRL+C zum Beenden\n") print("Drücke CTRL+C zum Beenden\n")
app.run(host='0.0.0.0', port=5003, debug=False) app.run(host='0.0.0.0', port=HTTP_PORT, debug=False)
if __name__ == '__main__': if __name__ == '__main__':

122
wetterstation_ingestion.py Normal file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Wetterstation Ingestion Service - HTTP-POST Datenempfang
Empfängt Wetterdaten via POST und speichert sie in der Datenbank
"""
import sqlite3
import os
from flask import Flask, jsonify, request
from dotenv import load_dotenv
# Lade Umgebungsvariablen aus .env Datei
load_dotenv()
# Konfiguration aus Umgebungsvariablen
DB_FILE = os.getenv("DB_FILE", "wetterdaten.db")
HTTP_PORT = int(os.getenv("INGESTION_PORT", 5004))
app = Flask(__name__)
app.url_map.strict_slashes = False
class WetterDB:
"""Klasse für Datenbankoperationen"""
def __init__(self, db_file):
self.db_file = db_file
self.init_db()
def init_db(self):
"""Datenbank initialisieren"""
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS wetterdaten (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateTime INTEGER NOT NULL,
barometer REAL,
outTemp REAL,
outHumidity INTEGER,
windSpeed REAL,
windDir REAL,
windGust REAL,
rainRate REAL,
rain REAL
)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_dateTime ON wetterdaten(dateTime)
''')
conn.commit()
conn.close()
def save_data(self, data):
"""Wetterdaten speichern"""
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO wetterdaten
(dateTime, barometer, outTemp, outHumidity, windSpeed, windDir, windGust, rainRate, rain)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data.get('dateTime'),
data.get('barometer'),
data.get('outTemp'),
data.get('outHumidity'),
data.get('windSpeed'),
data.get('windDir'),
data.get('windGust'),
data.get('rainRate'),
data.get('rain')
))
conn.commit()
conn.close()
print(f"Daten gespeichert: {data.get('dateTime')}")
# Globale Datenbankinstanz
db = WetterDB(DB_FILE)
# Flask Routes
@app.route('/health')
def health():
"""Health-Check Endpoint"""
return jsonify({'status': 'ok', 'service': 'ingestion'}), 200
# @app.route('/api/data/upload', methods=['POST'])
@app.route('/api/data/upload/', methods=['POST'])
def upload_data():
"""HTTP-POST Endpoint für Wetterdaten"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
# Daten speichern
db.save_data(data)
return jsonify({
'status': 'success',
'message': 'Daten empfangen und gespeichert'
}), 200
except Exception as e:
print(f"Fehler beim Verarbeiten der POST-Anfrage: {e}")
return jsonify({'error': str(e)}), 400
def main():
"""Hauptprogramm"""
print("Wetterstation Ingestion Service wird gestartet...")
print(f"\nHTTP-POST Endpoint: http://0.0.0.0:{HTTP_PORT}/api/data/upload")
print(f"Health-Check: http://0.0.0.0:{HTTP_PORT}/health")
print("Drücke CTRL+C zum Beenden\n")
app.run(host='0.0.0.0', port=HTTP_PORT, debug=False)
if __name__ == '__main__':
main()