Compare commits

..

3 Commits

5 changed files with 138 additions and 12 deletions

View File

@@ -18,6 +18,7 @@ services:
BAUD_RATE: ${BAUD_RATE:-19200} BAUD_RATE: ${BAUD_RATE:-19200}
DB_PATH: /data/wetter.db DB_PATH: /data/wetter.db
LOOP_INTERVAL_MS: ${LOOP_INTERVAL_MS:-30000} LOOP_INTERVAL_MS: ${LOOP_INTERVAL_MS:-30000}
COLLECTOR_API_KEY: ${COLLECTOR_API_KEY}
volumes: volumes:
wetter_data: wetter_data:

View File

@@ -166,8 +166,8 @@ function parseLOOP1(pkt) {
tempIn: temp(pkt.readInt16LE(9)), tempIn: temp(pkt.readInt16LE(9)),
humOut: hum(pkt[33]), humOut: hum(pkt[33]),
humIn: hum(pkt[11]), humIn: hum(pkt[11]),
windAvg: mph(pkt[14]), windAvg: mph(pkt[15]),
windGust: mph(pkt[15]), windGust: mph(pkt[14]),
windDir: pkt.readUInt16LE(16) || null, windDir: pkt.readUInt16LE(16) || null,
forecast: pkt[89], forecast: pkt[89],
pressure: press === 0 ? null : r1(press * 33.8639 / 1000), pressure: press === 0 ? null : r1(press * 33.8639 / 1000),

65
deploy.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Deploy Script für werte-next
# Baut das Docker Image und lädt es zu docker.citysensor.de hoch
set -e
# Konfiguration
REGISTRY="docker.citysensor.de"
IMAGE_NAME="wetterserver"
TAG="${TAG:-$(date +%Y%m%d%H%M)}" # default Datum
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
# Build-Datum
BUILD_DATE=$(date +%d.%m.%Y)
echo "=========================================="
echo "Werte-Next Deploy Script"
echo "=========================================="
echo "Registry: ${REGISTRY}"
echo "Image: ${IMAGE_NAME}"
echo "Tag: ${TAG}"
echo "Build-Datum: ${BUILD_DATE}"
echo "=========================================="
echo ""
# 1. Login zur Registry (falls noch nicht eingeloggt)
echo ">>> Login zu ${REGISTRY}..."
docker login "${REGISTRY}"
echo ""
# 2. Multiplatform Builder einrichten (docker-container driver erforderlich)
echo ">>> Richte Multiplatform Builder ein..."
if ! docker buildx inspect multiplatform-builder &>/dev/null; then
docker buildx create --name multiplatform-builder --driver docker-container --bootstrap
fi
docker buildx use multiplatform-builder
echo ""
# 3. Docker Image bauen und pushen (Multiplatform)
echo ">>> Baue Multiplatform Docker Image und pushe zu Registry..."
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg BUILD_DATE="${BUILD_DATE}" \
-t "${FULL_IMAGE}" \
--push \
.
# 4. Tagge auch als :latest
echo ">>> Tagge ${IMAGE_NAME} als :latest..."
docker buildx imagetools create \
-t "${REGISTRY}/${IMAGE_NAME}:latest" \
"${FULL_IMAGE}"
echo ">>> Build und Push erfolgreich!"
echo ""
echo "=========================================="
echo "✓ Deploy erfolgreich abgeschlossen!"
echo "=========================================="
echo ""
echo "Auf dem Server ausführen:"
echo " docker pull ${FULL_IMAGE}"
echo " docker-compose -f docker-compose.prod.yml up -d"
echo ""

View File

@@ -1,6 +1,6 @@
{ {
"name": "wetter_1", "name": "wetter_1",
"version": "1.1.0", "version": "1.2.0",
"description": "", "description": "",
"license": "ISC", "license": "ISC",
"author": "rxf", "author": "rxf",

View File

@@ -18,7 +18,9 @@ import { readArchiveSince, connectStation, fetchLoopData } from "./davis.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = process.env.DB_PATH ?? path.join(__dirname, "wetter.db"); const DB_PATH = process.env.DB_PATH ?? path.join(__dirname, "wetter.db");
const LOOP_INTERVAL_MS = Number(process.env.LOOP_INTERVAL_MS ?? 30_000); const LOOP_INTERVAL_MS = Number(process.env.LOOP_INTERVAL_MS ?? 30_000);
const DB_INTERVAL_MS = 5 * 60 * 1000; // alle 5 min in DB schreiben
const POST_URL = process.env.POST_URL ?? null; const POST_URL = process.env.POST_URL ?? null;
const COLLECTOR_API_KEY = process.env.COLLECTOR_API_KEY;
// ── Hilfsfunktionen ──────────────────────────────────────────────────────── // ── Hilfsfunktionen ────────────────────────────────────────────────────────
@@ -28,6 +30,54 @@ function log(msg) { console.log (`[${fmt24h(new Date())}] ${msg}`); }
function warn(msg) { console.warn(`[${fmt24h(new Date())}] WARN ${msg}`); } function warn(msg) { console.warn(`[${fmt24h(new Date())}] WARN ${msg}`); }
function err(msg) { console.error(`[${fmt24h(new Date())}] ERROR ${msg}`); } function err(msg) { console.error(`[${fmt24h(new Date())}] ERROR ${msg}`); }
function postData(data) {
if (!POST_URL) return;
const headers = { "Content-Type": "application/json" };
if (COLLECTOR_API_KEY) headers["X-API-Key"] = COLLECTOR_API_KEY;
fetch(POST_URL, { method: "POST", headers, body: JSON.stringify(data) })
.then(async res => {
if (!res.ok) {
const text = await res.text().catch(() => "");
warn(`POST ${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
}
})
.catch(e => warn("POST fehlgeschlagen: " + e.message));
}
// ── 5-Minuten-Aggregation ──────────────────────────────────────────────────
function aggregateBuffer(buf) {
const avg = (key) => {
const vals = buf.map(r => r[key]).filter(v => v !== null && v !== undefined);
if (!vals.length) return null;
return +( vals.reduce((a, b) => a + b, 0) / vals.length ).toFixed(1);
};
const peak = (key) => {
const vals = buf.map(r => r[key]).filter(v => v !== null && v !== undefined);
return vals.length ? +Math.max(...vals).toFixed(1) : null;
};
const last = (key) => {
for (let i = buf.length - 1; i >= 0; i--)
if (buf[i][key] !== null && buf[i][key] !== undefined) return buf[i][key];
return null;
};
return {
time: buf[buf.length - 1].time,
tempOut: avg('tempOut'),
tempIn: avg('tempIn'),
humOut: avg('humOut'),
humIn: avg('humIn'),
windAvg: avg('windAvg'),
windGust: peak('windGust'), // Spitzenwert der Periode
windDir: avg('windDir'),
pressure: avg('pressure'),
barTrend: last('barTrend'),
forecast: last('forecast'),
rain: last('rain'),
rainRate: avg('rainRate'),
};
}
// ── Archiv nachladen ─────────────────────────────────────────────────────── // ── Archiv nachladen ───────────────────────────────────────────────────────
async function catchUpArchive(db) { async function catchUpArchive(db) {
@@ -55,29 +105,39 @@ async function catchUpArchive(db) {
const inserted = insertRecords(db, records, "archive"); const inserted = insertRecords(db, records, "archive");
log(`Archiv: ${inserted} neue Datensätze gespeichert (${records.length} empfangen).`); log(`Archiv: ${inserted} neue Datensätze gespeichert (${records.length} empfangen).`);
for (const r of records) postData(r);
} }
// ── LOOP-Schleife ────────────────────────────────────────────────────────── // ── LOOP-Schleife ──────────────────────────────────────────────────────────
async function runLoop(db) { async function runLoop(db) {
let station = null; let station = null;
const buffer = [];
let lastDbWrite = Date.now();
async function connect() { async function connect() {
station = await connectStation(); station = await connectStation();
log("Verbunden mit Wetterstation."); log("Verbunden mit Davis-Console.");
} }
async function tick() { async function tick() {
try { try {
const data = await fetchLoopData(station); const data = await fetchLoopData(station);
insertRecord(db, data, "loop"); buffer.push(data);
if (POST_URL) {
fetch(POST_URL, { postData(data);
method: "POST",
headers: { "Content-Type": "application/json" }, const now = Date.now();
body: JSON.stringify(data), if (now - lastDbWrite >= DB_INTERVAL_MS) {
}).catch(e => warn("POST fehlgeschlagen: " + e.message)); const n = buffer.length;
const agg = aggregateBuffer(buffer);
buffer.length = 0;
lastDbWrite = now;
insertRecord(db, agg, "loop");
log(`DB: 5-min-Mittel gespeichert (${n} Messwerte)`);
} }
log( log(
`Außen: ${data.tempOut?.toFixed(1)}°C ` + `Außen: ${data.tempOut?.toFixed(1)}°C ` +
`InnenT: ${data.tempIn?.toFixed(1)}°C ` + `InnenT: ${data.tempIn?.toFixed(1)}°C ` +