diff --git a/DOCKER_README.md b/DOCKER_README.md index 1cd956f..7890dd1 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -1,26 +1,26 @@ # Wetterstation Docker Setup +## Architektur + +Die Wetterstation besteht aus zwei unabhängigen Services: + +1. **Ingestion Service** (Port 5004): Empfängt POST-Daten und schreibt in die Datenbank +2. **Web Service** (Port 5003): Stellt das Web-Interface und Lese-APIs bereit + +Beide Services teilen sich eine gemeinsame SQLite-Datenbank via Volume. + ## Voraussetzungen - Docker und Docker Compose installiert -- MQTT Broker Zugang (Host, Port, Benutzername, Passwort) ## Installation -### 1. `.env` Datei erstellen -Kopiere `.env.example` zu `.env` und fülle deine Daten ein: +### 1. `.env` Datei erstellen (optional) +Erstelle eine `.env` Datei für benutzerdefinierte Konfiguration: ```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 +HTTP_PORT=5003 +INGESTION_PORT=5004 ``` ### 2. Container starten @@ -28,34 +28,102 @@ DB_FILE=wetterdaten.db 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 ```bash -# Logs anschauen +# Logs anschauen (beide Services) docker-compose logs -f +# Logs nur Ingestion Service +docker-compose logs -f wetterstation-ingestion + +# Logs nur Web Service +docker-compose logs -f wetterstation-web + # Container stoppen docker-compose down # Container neustarten docker-compose restart + +# Nur Ingestion Service neustarten +docker-compose restart wetterstation-ingestion ``` ## Datenverwaltung -Die SQLite-Datenbank (`wetterdaten.db`) wird als Volume persistiert und bleibt erhalten, auch wenn 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 ### Datenbank-Fehler -Falls die Datenbank beschädigt ist, kannst du sie löschen und neu erstellen: +Falls die Datenbank beschädigt ist: ```bash rm wetterdaten.db -docker-compose restart +docker-compose restart wetterstation-ingestion ``` -### MQTT-Verbindungsfehler -Überprüfe deine `.env` Datei auf korrekte Credentials: +### Container neu bauen +Nach Code-Änderungen: ```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 +``` + diff --git a/Dockerfile.ingestion b/Dockerfile.ingestion new file mode 100644 index 0000000..5f593df --- /dev/null +++ b/Dockerfile.ingestion @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml index 027e52f..c2bba23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,29 @@ services: - wetterstation: - build: . - container_name: wetterstation + # Ingestion Service - empfängt POST-Daten + wetterstation-ingestion: + build: + context: . + dockerfile: Dockerfile.ingestion + container_name: wetterstation-ingestion + ports: + - "5004:5004" + volumes: + - ./wetterdaten.db:/app/wetterdaten.db + env_file: + - .env + environment: + - FLASK_ENV=production + - INGESTION_PORT=5004 + restart: unless-stopped + networks: + - wetterstation_network + + # Web Service - stellt UI und Lese-APIs bereit + wetterstation-web: + build: + context: . + dockerfile: Dockerfile + container_name: wetterstation-web ports: - "5003:5003" volumes: @@ -13,6 +35,8 @@ services: restart: unless-stopped networks: - wetterstation_network + depends_on: + - wetterstation-ingestion networks: wetterstation_network: diff --git a/requirements.txt b/requirements.txt index 1fe88fa..1269dab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ flask==3.0.0 -paho-mqtt==1.6.1 python-dotenv==1.0.0 \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 423bde1..763e24f 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -49,7 +49,7 @@ function renderCharts(apiData) { // Konvertiere Timestamps in Millisekunden const timestamps = data.map(d => { - const [date, time] = d.datetime.split(' '); + const [date, time] = d.dateTime.split(' '); return new Date(date + 'T' + time).getTime(); }); @@ -80,7 +80,7 @@ function renderCharts(apiData) { legend: { enabled: true }, series: [{ name: 'Temperatur', - data: data.map((d, i) => [timestamps[i], d.temperature]), + data: data.map((d, i) => [timestamps[i], d.outTemp]), color: '#ff6b6b', lineWidth: 2 }], @@ -101,7 +101,7 @@ function renderCharts(apiData) { legend: { enabled: true }, series: [{ name: 'Luftfeuchtigkeit', - data: data.map((d, i) => [timestamps[i], d.humidity]), + data: data.map((d, i) => [timestamps[i], d.outHumidity]), color: '#4ecdc4', lineWidth: 2 }], @@ -122,7 +122,7 @@ function renderCharts(apiData) { legend: { enabled: true }, series: [{ name: 'Luftdruck', - data: data.map((d, i) => [timestamps[i], d.pressure]), + data: data.map((d, i) => [timestamps[i], d.barometer]), color: '#95e1d3', lineWidth: 2 }], @@ -163,12 +163,12 @@ function renderCharts(apiData) { legend: { enabled: true }, series: [{ name: 'Windgeschwindigkeit', - data: data.map((d, i) => [timestamps[i], d.wind_speed]), + data: data.map((d, i) => [timestamps[i], d.windSpeed]), color: '#f38181', lineWidth: 2 }, { name: 'Böen', - data: data.map((d, i) => [timestamps[i], d.wind_gust]), + data: data.map((d, i) => [timestamps[i], d.windGust]), color: '#aa96da', lineWidth: 2, dashStyle: 'dash' @@ -178,7 +178,7 @@ function renderCharts(apiData) { // Windrichtung 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 (°)' }, xAxis: { type: 'datetime', @@ -195,7 +195,7 @@ function renderCharts(apiData) { legend: { enabled: true }, series: [{ 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', lineWidth: 2 }], diff --git a/wetterstation.py b/wetterstation.py index 737c5e2..a2c47b9 100644 --- a/wetterstation.py +++ b/wetterstation.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 """ -Wetterstation - HTTP-POST 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 json import os from datetime import datetime, timedelta -from flask import Flask, render_template, jsonify, request +from flask import Flask, render_template, jsonify from dotenv import load_dotenv # Lade Umgebungsvariablen aus .env Datei @@ -21,58 +22,10 @@ app = Flask(__name__) class WetterDB: - """Klasse für Datenbankoperationen""" + """Klasse für Datenbankoperationen (nur Lesezugriff)""" 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')}") def get_data(self, hours=24): """Daten der letzten X Stunden abrufen""" @@ -127,29 +80,10 @@ def index(): 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('/health') +def health(): + """Health-Check Endpoint""" + return jsonify({'status': 'ok', 'service': 'web'}), 200 @app.route('/api/data/') @@ -167,9 +101,12 @@ def get_historical_data(period): def main(): """Hauptprogramm""" - print("Wetterstation wird gestartet...") - print("\nWeb-Interface verfügbar unter: http://localhost:5003") - print("HTTP-POST Endpoint: http://localhost:5003/api/data/upload") + print("Wetterstation Web-Interface wird gestartet...") + print(f"\nWeb-Interface verfügbar unter: http://0.0.0.0:{HTTP_PORT}") + print(f"API Endpoints:") + print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/day") + print(f" - http://0.0.0.0:{HTTP_PORT}/api/data/week") + 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) diff --git a/wetterstation_ingestion.py b/wetterstation_ingestion.py new file mode 100644 index 0000000..851415d --- /dev/null +++ b/wetterstation_ingestion.py @@ -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()