First commit
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Datenbank
|
||||
DB_FILE=<database_name>
|
||||
|
||||
# HTTP-Port
|
||||
HTTP_PORT=<port>
|
||||
88
.gitignore
vendored
Normal file
88
.gitignore
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
*.sdb
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
lerna-debug.log
|
||||
.pnpm-debug.log
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.npm/
|
||||
|
||||
|
||||
# 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
|
||||
61
DOCKER_README.md
Normal file
61
DOCKER_README.md
Normal 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
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Multi-stage build: Leichtgewichtiger Container
|
||||
FROM node:18-alpine
|
||||
|
||||
# Setze Arbeitsverzeichnis
|
||||
WORKDIR /app
|
||||
|
||||
# Installiere Dependencies
|
||||
COPY package.json .
|
||||
RUN npm install --production
|
||||
|
||||
# Kopiere die Anwendung
|
||||
COPY server.js .
|
||||
COPY static/ static/
|
||||
COPY views/ views/
|
||||
|
||||
# Exponiere Port
|
||||
EXPOSE 5003
|
||||
|
||||
# Starten Sie die Anwendung
|
||||
CMD ["node", "server.js"]
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
wetterstation:
|
||||
build: .
|
||||
container_name: wetterstation
|
||||
ports:
|
||||
- "5003:5003"
|
||||
volumes:
|
||||
- ./wetterdaten.db:/app/wetterdaten.db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- wetterstation_network
|
||||
|
||||
networks:
|
||||
wetterstation_network:
|
||||
driver: bridge
|
||||
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)
|
||||
```
|
||||
2713
package-lock.json
generated
Normal file
2713
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "wetterstation",
|
||||
"version": "1.0.0",
|
||||
"description": "Wetterstation Web-Visualisierung (Node.js Port)",
|
||||
"type": "module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"pug": "^3.0.2",
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask==3.0.0
|
||||
paho-mqtt==1.6.1
|
||||
python-dotenv==1.0.0
|
||||
197
server.js
Normal file
197
server.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import sqlite3Pkg from 'sqlite3';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const sqlite3 = sqlite3Pkg.verbose();
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
|
||||
// Configuration
|
||||
const DB_FILE = process.env.DB_FILE || 'wetterdaten.db';
|
||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '5003', 10);
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use('/static', express.static(path.join(__dirname, 'static')));
|
||||
|
||||
// Database handling class
|
||||
class WetterDB {
|
||||
constructor(dbFile) {
|
||||
this.dbFile = dbFile;
|
||||
this.initDb();
|
||||
}
|
||||
|
||||
initDb() {
|
||||
const db = new sqlite3.Database(this.dbFile);
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS wetterdaten (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dateTime INTEGER NOT NULL,
|
||||
barometer REAL,
|
||||
outTemp REAL,
|
||||
outHumidity INTEGER,
|
||||
windSpeed REAL,
|
||||
windDir REAL,
|
||||
windGust REAL,
|
||||
rainRate REAL,
|
||||
rain REAL
|
||||
)
|
||||
`);
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_dateTime ON wetterdaten(dateTime)
|
||||
`);
|
||||
});
|
||||
db.close();
|
||||
}
|
||||
|
||||
saveData(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const db = new sqlite3.Database(this.dbFile);
|
||||
const sql = `
|
||||
INSERT INTO wetterdaten
|
||||
(dateTime, barometer, outTemp, outHumidity, windSpeed, windDir, windGust, rainRate, rain)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
data.dateTime,
|
||||
data.barometer,
|
||||
data.outTemp,
|
||||
data.outHumidity,
|
||||
data.windSpeed,
|
||||
data.windDir,
|
||||
data.windGust,
|
||||
data.rainRate,
|
||||
data.rain
|
||||
];
|
||||
|
||||
db.run(sql, params, function(err) {
|
||||
db.close();
|
||||
if (err) {
|
||||
console.error("Error saving data:", err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`Daten gespeichert: ${data.dateTime}`);
|
||||
resolve(this.lastID);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getData(hours = 24) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const db = new sqlite3.Database(this.dbFile);
|
||||
|
||||
// Calculate timestamp threshold (current time - hours) in seconds (Unix Timestamp)
|
||||
const timeThreshold = Math.floor((Date.now() - hours * 60 * 60 * 1000) / 1000);
|
||||
|
||||
const sql = `
|
||||
SELECT * FROM wetterdaten
|
||||
WHERE dateTime >= ?
|
||||
ORDER BY dateTime ASC
|
||||
`;
|
||||
|
||||
db.all(sql, [timeThreshold], (err, rows) => {
|
||||
db.close();
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getHourlyRain(hours = 24) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const db = new sqlite3.Database(this.dbFile);
|
||||
|
||||
// Calculate timestamp threshold (current time - hours) in seconds (Unix Timestamp)
|
||||
const timeThreshold = Math.floor((Date.now() - hours * 60 * 60 * 1000) / 1000);
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
strftime('%Y-%m-%d %H:00:00', datetime(dateTime, 'unixepoch', 'localtime')) as hour,
|
||||
SUM(rainRate) as total_rain
|
||||
FROM wetterdaten
|
||||
WHERE dateTime >= ?
|
||||
GROUP BY hour
|
||||
ORDER BY hour ASC
|
||||
`;
|
||||
|
||||
db.all(sql, [timeThreshold], (err, rows) => {
|
||||
db.close();
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const result = rows.map(row => ({
|
||||
hour: row.hour,
|
||||
rain: row.total_rain || 0
|
||||
}));
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global DB instance
|
||||
const db = new WetterDB(DB_FILE);
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
|
||||
app.post('/api/data/upload', async (req, res) => {
|
||||
try {
|
||||
const data = req.body;
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Daten empfangen' });
|
||||
}
|
||||
|
||||
await db.saveData(data);
|
||||
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
message: 'Daten empfangen und gespeichert'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Fehler beim Verarbeiten der POST-Anfrage: ${e}`);
|
||||
res.status(400).json({ error: e.toString() });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/data/:period', async (req, res) => {
|
||||
const period = req.params.period;
|
||||
const hours = period === 'day' ? 24 : 168; // 168h = 1 week
|
||||
|
||||
try {
|
||||
const [data, rainData] = await Promise.all([
|
||||
db.getData(hours),
|
||||
db.getHourlyRain(hours)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
data: data,
|
||||
rain_hourly: rainData
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(HTTP_PORT, '0.0.0.0', () => {
|
||||
console.log("Wetterstation wird gestartet...");
|
||||
console.log(`\nWeb-Interface verfügbar unter: http://localhost:${HTTP_PORT}`);
|
||||
console.log(`HTTP-POST Endpoint: http://localhost:${HTTP_PORT}/api/data/upload`);
|
||||
console.log("Drücke CTRL+C zum Beenden\n");
|
||||
});
|
||||
89
static/css/style.css
Normal file
89
static/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
213
static/js/app.js
Normal file
213
static/js/app.js
Normal file
@@ -0,0 +1,213 @@
|
||||
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 Unix Timestamps (Sekunden) in Millisekunden für JS
|
||||
const timestamps = data.map(d => {
|
||||
// Wenn es schon Millisekunden sind (sehr große Zahl, > 10^11), lassen, sonst * 1000
|
||||
return d.dateTime > 10000000000 ? d.dateTime : d.dateTime * 1000;
|
||||
});
|
||||
|
||||
const rainTimestamps = rainData.map(d => {
|
||||
// rainData.hour kommt aus SQLite "strftime" als String "YYYY-MM-DD HH:00:00"
|
||||
// da wir unixepoch verwendet haben, müssen wir prüfen, was der Server liefert.
|
||||
// Server liefert "YYYY-MM-DD HH:00:00". Das müssen wir parsen.
|
||||
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.outTemp]),
|
||||
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.outHumidity]),
|
||||
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.barometer]),
|
||||
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.windSpeed]),
|
||||
color: '#f38181',
|
||||
lineWidth: 2
|
||||
}, {
|
||||
name: 'Böen',
|
||||
data: data.map((d, i) => [timestamps[i], d.windGust]),
|
||||
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.windDir || 0]),
|
||||
color: '#f39c12',
|
||||
lineWidth: 2
|
||||
}],
|
||||
credits: { enabled: false }
|
||||
});
|
||||
}
|
||||
|
||||
// Initiales Laden
|
||||
loadData();
|
||||
|
||||
// Auto-Refresh alle 5 Minuten
|
||||
setInterval(loadData, 5 * 60 * 1000);
|
||||
35
views/index.pug
Normal file
35
views/index.pug
Normal file
@@ -0,0 +1,35 @@
|
||||
doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="UTF-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||
title Wetterstation
|
||||
link(rel="stylesheet", href="/static/css/style.css")
|
||||
script(src="https://code.highcharts.com/highcharts.js")
|
||||
script(src="https://code.highcharts.com/modules/exporting.js")
|
||||
script(src="https://code.highcharts.com/modules/export-data.js")
|
||||
body
|
||||
.container
|
||||
h1 🌤️ Wetterstation Dashboard
|
||||
|
||||
.tabs
|
||||
button.tab.active(onclick="switchPeriod('day')") Tag (24h)
|
||||
button.tab(onclick="switchPeriod('week')") Woche (7 Tage)
|
||||
|
||||
#loading.loading Lade Daten...
|
||||
|
||||
#charts.charts-grid(style="display: none;")
|
||||
.chart-container
|
||||
#temp-chart
|
||||
.chart-container
|
||||
#humidity-chart
|
||||
.chart-container
|
||||
#pressure-chart
|
||||
.chart-container
|
||||
#rain-chart
|
||||
.chart-container
|
||||
#wind-speed-chart
|
||||
.chart-container
|
||||
#wind-dir-chart
|
||||
|
||||
script(src="/static/js/app.js")
|
||||
Reference in New Issue
Block a user