First commit
This commit is contained in:
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Database
|
||||||
|
wetterdaten.db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
templates/index.html
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
90
docs/README.md
Normal file
90
docs/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Wetterstation - MQTT zu Web Dashboard
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Python-Pakete installieren:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Öffne `wetterstation.py` und passe folgende Zeilen an:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MQTT_TOPIC = "wetter/daten" # Dein MQTT Topic
|
||||||
|
MQTT_USER = "username" # Dein MQTT Benutzername
|
||||||
|
MQTT_PASSWORD = "password" # Dein MQTT Passwort
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
1. **Programm starten:**
|
||||||
|
```bash
|
||||||
|
python wetterstation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Browser öffnen:**
|
||||||
|
- Öffne http://localhost:5000
|
||||||
|
- Du siehst das Dashboard mit allen Grafiken
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
### MQTT Datenempfang
|
||||||
|
- Verbindet sich automatisch mit rexfue.de
|
||||||
|
- Empfängt Wetterdaten im JSON-Format
|
||||||
|
- Speichert alle Daten in SQLite Datenbank
|
||||||
|
|
||||||
|
### Web-Dashboard
|
||||||
|
- **6 Grafiken:**
|
||||||
|
- Temperatur über Zeit
|
||||||
|
- Luftfeuchtigkeit über Zeit
|
||||||
|
- Luftdruck über Zeit
|
||||||
|
- Regenmenge pro Stunde (Balkendiagramm)
|
||||||
|
- Windgeschwindigkeit + Böen
|
||||||
|
- Windrichtung (Polarplot)
|
||||||
|
|
||||||
|
- **Zwei Ansichten:**
|
||||||
|
- Tag: Letzte 24 Stunden
|
||||||
|
- Woche: Letzte 7 Tage
|
||||||
|
- Umschaltbar per Tab
|
||||||
|
|
||||||
|
- **Auto-Refresh:** Aktualisiert sich alle 5 Minuten automatisch
|
||||||
|
|
||||||
|
## Datenbank
|
||||||
|
|
||||||
|
Die Wetterdaten werden in `wetterdaten.db` gespeichert (SQLite).
|
||||||
|
|
||||||
|
## Erwartetes MQTT Datenformat
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"datetime": "2026-01-24 16:05:00",
|
||||||
|
"pressure": 1002.0,
|
||||||
|
"wind_gust": 5.0,
|
||||||
|
"wind_speed": 3.0,
|
||||||
|
"wind_dir": 45.0,
|
||||||
|
"rain_rate": 0.0,
|
||||||
|
"rain": 0.0,
|
||||||
|
"humidity": 80,
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Problemlösung
|
||||||
|
|
||||||
|
**MQTT verbindet nicht:**
|
||||||
|
- Prüfe MQTT_USER und MQTT_PASSWORD
|
||||||
|
- Prüfe MQTT_TOPIC
|
||||||
|
- Stelle sicher, dass rexfue.de erreichbar ist
|
||||||
|
|
||||||
|
**Keine Daten im Dashboard:**
|
||||||
|
- Warte bis erste MQTT Nachricht empfangen wurde
|
||||||
|
- Prüfe die Konsole auf Fehlermeldungen
|
||||||
|
- Überprüfe ob `wetterdaten.db` erstellt wurde
|
||||||
|
|
||||||
|
**Port 5000 bereits belegt:**
|
||||||
|
Ändere in `wetterstation.py` die letzte Zeile:
|
||||||
|
```python
|
||||||
|
app.run(host='0.0.0.0', port=8080, debug=False)
|
||||||
|
```
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
paho-mqtt==1.6.1
|
||||||
571
wetterstation.py
Normal file
571
wetterstation.py
Normal 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()
|
||||||
Reference in New Issue
Block a user