First commit

This commit is contained in:
rxf
2026-01-24 20:18:23 +01:00
commit d6b718a8ef
4 changed files with 735 additions and 0 deletions

571
wetterstation.py Normal file
View File

@@ -0,0 +1,571 @@
#!/usr/bin/env python3
"""
Wetterstation - MQTT Datenempfang und Web-Visualisierung
"""
import sqlite3
import json
import threading
from datetime import datetime, timedelta
from flask import Flask, render_template, jsonify
import paho.mqtt.client as mqtt
# Konfiguration
MQTT_HOST = "rexfue.de"
MQTT_PORT = 1883
MQTT_TOPIC = "vantage/live" # Bitte anpassen!
MQTT_USER = "stzuhr" # Bitte anpassen!
MQTT_PASSWORD = "74chQCYb" # Bitte anpassen!
DB_FILE = "wetterdaten.db"
app = Flask(__name__)
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 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):
"""Daten der letzten X Stunden abrufen"""
conn = sqlite3.connect(self.db_file)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
time_threshold = (datetime.now() - timedelta(hours=hours)).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute('''
SELECT * FROM wetterdaten
WHERE datetime >= ?
ORDER BY datetime ASC
''', (time_threshold,))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_hourly_rain(self, hours=24):
"""Regenmenge pro Stunde berechnen"""
conn = sqlite3.connect(self.db_file)
cursor = conn.cursor()
time_threshold = (datetime.now() - timedelta(hours=hours)).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute('''
SELECT
strftime('%Y-%m-%d %H:00:00', datetime) as hour,
SUM(rain_rate) as total_rain
FROM wetterdaten
WHERE datetime >= ?
GROUP BY hour
ORDER BY hour ASC
''', (time_threshold,))
rows = cursor.fetchall()
conn.close()
return [{'hour': row[0], 'rain': row[1] or 0} for row in rows]
# Globale Datenbankinstanz
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
@app.route('/')
def index():
"""Hauptseite"""
return render_template('index.html')
@app.route('/api/data/<period>')
def get_data(period):
"""API Endpoint für Wetterdaten"""
hours = 24 if period == 'day' else 168 # 168h = 1 Woche
data = db.get_data(hours)
rain_data = db.get_hourly_rain(hours)
return jsonify({
'data': data,
'rain_hourly': rain_data
})
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():
"""Hauptprogramm"""
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("Drücke CTRL+C zum Beenden\n")
app.run(host='0.0.0.0', port=5003, debug=False)
if __name__ == '__main__':
main()