Compare commits

...

5 Commits

Author SHA1 Message Date
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
12 changed files with 1026 additions and 457 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

61
DOCKER_README.md Normal file
View File

@@ -0,0 +1,61 @@
# Wetterstation Docker Setup
## Voraussetzungen
- Docker und Docker Compose installiert
- MQTT Broker Zugang (Host, Port, Benutzername, Passwort)
## Installation
### 1. `.env` Datei erstellen
Kopiere `.env.example` zu `.env` und fülle deine Daten ein:
```bash
cp .env.example .env
```
Bearbeite `.env` mit deinen MQTT-Credentials:
```
MQTT_HOST=dein_broker.com
MQTT_PORT=1883
MQTT_TOPIC=vantage/live
MQTT_USER=dein_benutzer
MQTT_PASSWORD=dein_passwort
DB_FILE=wetterdaten.db
```
### 2. Container starten
```bash
docker-compose up -d
```
Die Anwendung läuft dann unter `http://localhost:5003`
### 3. Container verwalten
```bash
# Logs anschauen
docker-compose logs -f
# Container stoppen
docker-compose down
# Container neustarten
docker-compose restart
```
## Datenverwaltung
Die SQLite-Datenbank (`wetterdaten.db`) wird als Volume persistiert und bleibt erhalten, auch wenn der Container gelöscht wird.
## Troubleshooting
### Datenbank-Fehler
Falls die Datenbank beschädigt ist, kannst du sie löschen und neu erstellen:
```bash
rm wetterdaten.db
docker-compose restart
```
### MQTT-Verbindungsfehler
Überprüfe deine `.env` Datei auf korrekte Credentials:
```bash
docker-compose logs wetterstation | grep -i mqtt
```

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"]

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
wetterstation:
build: .
container_name: wetterstation
ports:
- "5003:5003"
volumes:
- ./wetterdaten.db:/app/wetterdaten.db
env_file:
- .env
environment:
- FLASK_ENV=production
restart: unless-stopped
networks:
- wetterstation_network
networks:
wetterstation_network:
driver: bridge

View File

@@ -1,2 +1,3 @@
flask==3.0.0 flask==3.0.0
paho-mqtt==1.6.1 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;
}
}

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

@@ -0,0 +1,210 @@
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 => {
const [date, time] = d.datetime.split(' ');
return new Date(date + 'T' + time).getTime();
});
const rainTimestamps = rainData.map(d => {
const [date, time] = d.hour.split(' ');
return new Date(date + 'T' + time).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
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
},
yAxis: { title: { text: '°C' } },
legend: { enabled: true },
series: [{
name: 'Temperatur',
data: data.map((d, i) => [timestamps[i], d.temperature]),
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
},
yAxis: { title: { text: '%' } },
legend: { enabled: true },
series: [{
name: 'Luftfeuchtigkeit',
data: data.map((d, i) => [timestamps[i], d.humidity]),
color: '#4ecdc4',
lineWidth: 2
}],
credits: { enabled: false }
});
// Luftdruck
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
},
yAxis: { title: { text: 'hPa' } },
legend: { enabled: true },
series: [{
name: 'Luftdruck',
data: data.map((d, i) => [timestamps[i], d.pressure]),
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
},
yAxis: { title: { text: 'mm' } },
legend: { enabled: false },
series: [{
name: 'Regen',
data: rainData.map((d, i) => [rainTimestamps[i], d.rain]),
color: '#3498db'
}],
credits: { enabled: false }
});
// Windgeschwindigkeit
Highcharts.chart('wind-speed-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '💨 Windgeschwindigkeit (m/s)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS
},
yAxis: { title: { text: 'm/s' } },
legend: { enabled: true },
series: [{
name: 'Windgeschwindigkeit',
data: data.map((d, i) => [timestamps[i], d.wind_speed]),
color: '#f38181',
lineWidth: 2
}, {
name: 'Böen',
data: data.map((d, i) => [timestamps[i], d.wind_gust]),
color: '#aa96da',
lineWidth: 2,
dashStyle: 'dash'
}],
credits: { enabled: false }
});
// Windrichtung
Highcharts.chart('wind-dir-chart', {
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
title: { text: '🧭 Windrichtung (°)' },
xAxis: {
type: 'datetime',
title: { text: 'Zeit' },
labels: { format: '{value:%H}' },
tickInterval: FOUR_HOURS
},
yAxis: {
title: { text: 'Richtung (°)' },
min: 0,
max: 360,
tickPositions: [0, 90, 180, 270, 360]
},
legend: { enabled: true },
series: [{
name: 'Windrichtung',
data: data.map((d, i) => [timestamps[i], d.wind_dir || 0]),
color: '#f39c12',
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,23 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Wetterstation - MQTT Datenempfang und Web-Visualisierung Wetterstation - HTTP-POST Datenempfang und Web-Visualisierung
""" """
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, request
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__)
@@ -36,19 +34,19 @@ class WetterDB:
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS wetterdaten ( CREATE TABLE IF NOT EXISTS wetterdaten (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT NOT NULL, dateTime TEXT NOT NULL,
pressure REAL, barometer REAL,
wind_gust REAL, outTemp REAL,
wind_speed REAL, outHumidity INTEGER,
wind_dir REAL, windSpeed REAL,
rain_rate REAL, windDir REAL,
rain REAL, windGust REAL,
humidity INTEGER, rainRate REAL,
temperature REAL rain REAL
) )
''') ''')
cursor.execute(''' cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_datetime ON wetterdaten(datetime) CREATE INDEX IF NOT EXISTS idx_dateTime ON wetterdaten(dateTime)
''') ''')
conn.commit() conn.commit()
conn.close() conn.close()
@@ -59,23 +57,22 @@ class WetterDB:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
INSERT INTO wetterdaten INSERT INTO wetterdaten
(datetime, pressure, wind_gust, wind_speed, wind_dir, (dateTime, barometer, outTemp, outHumidity, windSpeed, windDir, windGust, rainRate, rain)
rain_rate, rain, humidity, temperature)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
data['datetime'], data.get('dateTime'),
data.get('pressure'), data.get('barometer'),
data.get('wind_gust'), data.get('outTemp'),
data.get('wind_speed'), data.get('outHumidity'),
data.get('wind_dir'), data.get('windSpeed'),
data.get('rain_rate'), data.get('windDir'),
data.get('rain'), data.get('windGust'),
data.get('humidity'), data.get('rainRate'),
data.get('temperature') data.get('rain')
)) ))
conn.commit() conn.commit()
conn.close() conn.close()
print(f"Daten gespeichert: {data['datetime']}") print(f"Daten gespeichert: {data.get('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"""
@@ -87,8 +84,8 @@ class WetterDB:
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()
@@ -105,10 +102,10 @@ class WetterDB:
cursor.execute(''' cursor.execute('''
SELECT SELECT
strftime('%Y-%m-%d %H:00:00', datetime) as hour, strftime('%Y-%m-%d %H:00:00', dateTime) 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 +120,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 +127,34 @@ def index():
return render_template('index.html') return render_template('index.html')
@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 (unverändert)
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
@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 +165,13 @@ 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 wird gestartet...")
# HTML Template erstellen
create_html_template()
# MQTT Client starten
mqtt_client = MQTTClient()
mqtt_client.start()
# Flask Server starten
print("\nWeb-Interface verfügbar unter: http://localhost:5003") print("\nWeb-Interface verfügbar unter: http://localhost:5003")
print("HTTP-POST Endpoint: http://localhost:5003/api/data/upload")
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__':