Compare commits
2 Commits
c6c3f4cf37
...
511cc31dc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 511cc31dc0 | |||
| c3614ebab0 |
110
DOCKER_README.md
110
DOCKER_README.md
@@ -1,26 +1,26 @@
|
|||||||
# Wetterstation Docker Setup
|
# 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
|
## Voraussetzungen
|
||||||
- Docker und Docker Compose installiert
|
- Docker und Docker Compose installiert
|
||||||
- MQTT Broker Zugang (Host, Port, Benutzername, Passwort)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### 1. `.env` Datei erstellen
|
### 1. `.env` Datei erstellen (optional)
|
||||||
Kopiere `.env.example` zu `.env` und fülle deine Daten ein:
|
Erstelle eine `.env` Datei für benutzerdefinierte Konfiguration:
|
||||||
|
|
||||||
```bash
|
```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
|
DB_FILE=wetterdaten.db
|
||||||
|
HTTP_PORT=5003
|
||||||
|
INGESTION_PORT=5004
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Container starten
|
### 2. Container starten
|
||||||
@@ -28,34 +28,102 @@ DB_FILE=wetterdaten.db
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Anwendung läuft dann unter `http://localhost:5003`
|
Die Services laufen dann unter:
|
||||||
|
- Web-Interface: `http://localhost:5003`
|
||||||
|
- Ingestion API: `http://localhost:5004/api/data/upload`
|
||||||
|
|
||||||
### 3. Container verwalten
|
### 3. Container verwalten
|
||||||
```bash
|
```bash
|
||||||
# Logs anschauen
|
# Logs anschauen (beide Services)
|
||||||
docker-compose logs -f
|
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
|
# Container stoppen
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
|
||||||
# Container neustarten
|
# Container neustarten
|
||||||
docker-compose restart
|
docker-compose restart
|
||||||
|
|
||||||
|
# Nur Ingestion Service neustarten
|
||||||
|
docker-compose restart wetterstation-ingestion
|
||||||
```
|
```
|
||||||
|
|
||||||
## Datenverwaltung
|
## Datenverwaltung
|
||||||
Die SQLite-Datenbank (`wetterdaten.db`) wird als Volume persistiert und bleibt erhalten, auch wenn der Container gelöscht wird.
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### Datenbank-Fehler
|
### Datenbank-Fehler
|
||||||
Falls die Datenbank beschädigt ist, kannst du sie löschen und neu erstellen:
|
Falls die Datenbank beschädigt ist:
|
||||||
```bash
|
```bash
|
||||||
rm wetterdaten.db
|
rm wetterdaten.db
|
||||||
docker-compose restart
|
docker-compose restart wetterstation-ingestion
|
||||||
```
|
```
|
||||||
|
|
||||||
### MQTT-Verbindungsfehler
|
### Container neu bauen
|
||||||
Überprüfe deine `.env` Datei auf korrekte Credentials:
|
Nach Code-Änderungen:
|
||||||
```bash
|
```bash
|
||||||
docker-compose logs wetterstation | grep -i mqtt
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
18
Dockerfile.ingestion
Normal file
18
Dockerfile.ingestion
Normal 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"]
|
||||||
@@ -1,7 +1,29 @@
|
|||||||
services:
|
services:
|
||||||
wetterstation:
|
# Ingestion Service - empfängt POST-Daten
|
||||||
build: .
|
wetterstation-ingestion:
|
||||||
container_name: wetterstation
|
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:
|
ports:
|
||||||
- "5003:5003"
|
- "5003:5003"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -13,6 +35,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- wetterstation_network
|
- wetterstation_network
|
||||||
|
depends_on:
|
||||||
|
- wetterstation-ingestion
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
wetterstation_network:
|
wetterstation_network:
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
flask==3.0.0
|
flask==3.0.0
|
||||||
paho-mqtt==1.6.1
|
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
@@ -49,7 +49,7 @@ function renderCharts(apiData) {
|
|||||||
|
|
||||||
// Konvertiere Timestamps in Millisekunden
|
// Konvertiere Timestamps in Millisekunden
|
||||||
const timestamps = data.map(d => {
|
const timestamps = data.map(d => {
|
||||||
const [date, time] = d.datetime.split(' ');
|
const [date, time] = d.dateTime.split(' ');
|
||||||
return new Date(date + 'T' + time).getTime();
|
return new Date(date + 'T' + time).getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ function renderCharts(apiData) {
|
|||||||
legend: { enabled: true },
|
legend: { enabled: true },
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Temperatur',
|
name: 'Temperatur',
|
||||||
data: data.map((d, i) => [timestamps[i], d.temperature]),
|
data: data.map((d, i) => [timestamps[i], d.outTemp]),
|
||||||
color: '#ff6b6b',
|
color: '#ff6b6b',
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
}],
|
}],
|
||||||
@@ -101,7 +101,7 @@ function renderCharts(apiData) {
|
|||||||
legend: { enabled: true },
|
legend: { enabled: true },
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Luftfeuchtigkeit',
|
name: 'Luftfeuchtigkeit',
|
||||||
data: data.map((d, i) => [timestamps[i], d.humidity]),
|
data: data.map((d, i) => [timestamps[i], d.outHumidity]),
|
||||||
color: '#4ecdc4',
|
color: '#4ecdc4',
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
}],
|
}],
|
||||||
@@ -122,7 +122,7 @@ function renderCharts(apiData) {
|
|||||||
legend: { enabled: true },
|
legend: { enabled: true },
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Luftdruck',
|
name: 'Luftdruck',
|
||||||
data: data.map((d, i) => [timestamps[i], d.pressure]),
|
data: data.map((d, i) => [timestamps[i], d.barometer]),
|
||||||
color: '#95e1d3',
|
color: '#95e1d3',
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
}],
|
}],
|
||||||
@@ -163,12 +163,12 @@ function renderCharts(apiData) {
|
|||||||
legend: { enabled: true },
|
legend: { enabled: true },
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Windgeschwindigkeit',
|
name: 'Windgeschwindigkeit',
|
||||||
data: data.map((d, i) => [timestamps[i], d.wind_speed]),
|
data: data.map((d, i) => [timestamps[i], d.windSpeed]),
|
||||||
color: '#f38181',
|
color: '#f38181',
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
}, {
|
}, {
|
||||||
name: 'Böen',
|
name: 'Böen',
|
||||||
data: data.map((d, i) => [timestamps[i], d.wind_gust]),
|
data: data.map((d, i) => [timestamps[i], d.windGust]),
|
||||||
color: '#aa96da',
|
color: '#aa96da',
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
dashStyle: 'dash'
|
dashStyle: 'dash'
|
||||||
@@ -178,7 +178,7 @@ function renderCharts(apiData) {
|
|||||||
|
|
||||||
// Windrichtung
|
// Windrichtung
|
||||||
Highcharts.chart('wind-dir-chart', {
|
Highcharts.chart('wind-dir-chart', {
|
||||||
chart: { type: 'line', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
|
chart: { type: 'scatter', height: DEFAULT_CHART_HEIGHT, spacingRight: 20 },
|
||||||
title: { text: '🧭 Windrichtung (°)' },
|
title: { text: '🧭 Windrichtung (°)' },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
@@ -195,7 +195,7 @@ function renderCharts(apiData) {
|
|||||||
legend: { enabled: true },
|
legend: { enabled: true },
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Windrichtung',
|
name: 'Windrichtung',
|
||||||
data: data.map((d, i) => [timestamps[i], d.wind_dir || 0]),
|
data: data.map((d, i) => [timestamps[i], d.windDir || 0]),
|
||||||
color: '#f39c12',
|
color: '#f39c12',
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
}],
|
}],
|
||||||
|
|||||||
200
wetterstation.py
200
wetterstation.py
@@ -1,86 +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
|
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
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Lade Umgebungsvariablen aus .env Datei
|
# Lade Umgebungsvariablen aus .env Datei
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Konfiguration aus Umgebungsvariablen
|
# Konfiguration aus Umgebungsvariablen
|
||||||
MQTT_HOST = os.getenv("MQTT_HOST", "rexfue.de")
|
|
||||||
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
|
|
||||||
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "vantage/live")
|
|
||||||
MQTT_USER = os.getenv("MQTT_USER", "")
|
|
||||||
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "")
|
|
||||||
|
|
||||||
DB_FILE = os.getenv("DB_FILE", "wetterdaten.db")
|
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"""
|
||||||
@@ -92,8 +37,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()
|
||||||
@@ -110,10 +55,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,))
|
||||||
@@ -128,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():
|
||||||
@@ -233,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)
|
||||||
@@ -246,21 +99,16 @@ def get_data(period):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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}")
|
||||||
# MQTT Client starten
|
print(f"API Endpoints:")
|
||||||
mqtt_client = MQTTClient()
|
print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/day")
|
||||||
mqtt_client.start()
|
print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/week")
|
||||||
|
print(f"Health-Check: http://0.0.0.0:{HTTP_PORT}/health")
|
||||||
# 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
122
wetterstation_ingestion.py
Normal 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 TEXT 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()
|
||||||
Reference in New Issue
Block a user