commit d1cfee0dea38652c0d5edd26a81adb6392cd9124 Author: Reinhard X. Fürst Date: Fri Apr 3 22:24:22 2026 +0200 First commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dc624ff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# Lokale Umgebungsdatei – wird NICHT in den Container kopiert +.env.local + +# Datenbank-Dateien +*.db +*.db-shm +*.db-wal + +# Node +node_modules/ +npm-debug.log* + +# Testdateien +*_test.mjs diff --git a/.env b/.env new file mode 100644 index 0000000..358e9b2 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# Davis Vantage Pro 2 – Konfiguration + +# Serieller Port der Wetterstation +PORT_PATH=/dev/ttyUSB0 + +# Baudrate (Davis Standard: 19200) +BAUD_RATE=19200 + +# Pfad zur SQLite-Datenbank +DB_PATH=/data/wetter.db + +# Abfrageintervall für LOOP-Daten in Millisekunden (Standard: 30s) +LOOP_INTERVAL_MS=30000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..001cb1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Abhängigkeiten +node_modules/ + +# Lokale Konfiguration +.env.local + +# Datenbank +*.db +*.db-shm +*.db-wal + +# Logs +npm-debug.log* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..48daf6f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/wetter.js" + }, + + { + "type": "node", + "request": "launch", + "name": "Launch archive", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/archive.js" + } + + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..697e8c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# ── Build-Stage ──────────────────────────────────────────────────────────── +FROM node:24-bookworm-slim AS build + +WORKDIR /app + +# Nur package*.json kopieren → besseres Layer-Caching +COPY package.json package-lock.json ./ + +# Native Module (better-sqlite3, serialport) kompilieren +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ \ + && npm ci --omit=dev \ + && apt-get purge -y python3 make g++ \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +# ── Runtime-Stage ─────────────────────────────────────────────────────────── +FROM node:24-bookworm-slim + +WORKDIR /app + +# node_modules aus Build-Stage übernehmen +COPY --from=build /app/node_modules ./node_modules + +# Anwendungscode kopieren +COPY davis.js db.js wetter.js .env ./ + +# Datenbankverzeichnis als Volume +VOLUME ["/data"] + +# Kein Port – reine Backend-Anwendung + +CMD ["node", "wetter.js"] diff --git a/archive.js b/archive.js new file mode 100644 index 0000000..76f05a9 --- /dev/null +++ b/archive.js @@ -0,0 +1,37 @@ +import { readArchiveSince } from "./davis.js"; + +// ── Ausgabe ──────────────────────────────────────────────────────────────── + +function formatRecord(r) { + const ts = r.time.toLocaleString("de-DE", { hour12: false }); + const tmp = r.tempOut !== null ? `${r.tempOut}°C` : "n/a"; + const hum = r.humOut !== null ? `${r.humOut}%` : "n/a"; + const wnd = r.windAvg !== null ? `${r.windAvg} km/h` : "n/a"; + const dir = r.windDir ?? "n/a"; + const pre = r.pressure !== null ? `${r.pressure} hPa` : "n/a"; + const rai = r.rain > 0 ? ` Regen: ${r.rain}mm` : ""; + return `${ts} Außen: ${tmp} Feuchte: ${hum} Wind: ${wnd} ${dir} Druck: ${pre}${rai}`; +} + +// ── CLI ──────────────────────────────────────────────────────────────────── +// Aufruf: node archive.js [ISO-Datum] +// Beispiel: node archive.js 2026-04-01T00:00:00 + +let since = new Date(Date.now() - 24 * 60 * 60 * 1000); +if (process.argv[2]) { + since = new Date(process.argv[2]); + if (isNaN(since)) { console.error("Ungültiges Datum:", process.argv[2]); process.exit(1); } +} + +console.error(`Lese Archiv ab ${since.toLocaleString("de-DE", { hour12: false })} ...`); + +let lastPct = -1; +const records = await readArchiveSince(since, (cur, total) => { + const pct = Math.floor(cur / total * 100); + if (pct !== lastPct) { process.stderr.write(`\r${pct}% (Seite ${cur}/${total})`); lastPct = pct; } +}); +process.stderr.write("\r\x1b[K"); + +console.error(`${records.length} Datensätze gefunden.`); +console.log(); +for (const r of records) console.log(formatRecord(r)); diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..3db79d4 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,23 @@ +services: + wetter: + build: . + container_name: wetter + restart: unless-stopped + + # Seriellen Port durchreichen + devices: + - "${PORT_PATH:-/dev/ttyUSB0}:/dev/ttyUSB0" + + # Datenbank-Volume + volumes: + - wetter_data:/data + + # Umgebungsvariablen (überschreiben .env im Image) + environment: + PORT_PATH: /dev/ttyUSB0 + BAUD_RATE: ${BAUD_RATE:-19200} + DB_PATH: /data/wetter.db + LOOP_INTERVAL_MS: ${LOOP_INTERVAL_MS:-30000} + +volumes: + wetter_data: diff --git a/davis.js b/davis.js new file mode 100644 index 0000000..2dae1c3 --- /dev/null +++ b/davis.js @@ -0,0 +1,298 @@ +/** + * davis.js – Gemeinsames Modul für die serielle Kommunikation mit der + * Davis Vantage Pro 2 Wetterstation. + * + * Exports: + * PORT_PATH, BAUD_RATE – Verbindungseinstellungen + * makeReader(port) – Byte-Accumulator für einen Raw-SerialPort + * wakeUp(port, readBytes) – Wakeup-Sequenz (Raw-Port, 3 Versuche) + * connectStation() – Raw SerialPort öffnen; gibt {port, readBytes, disconnect} zurück + * wakeUpStation(station) – Wakeup über station.port / station.readBytes + * fetchLoopData(station) – LOOP1-Datensatz direkt lesen & normalisieren + * readArchiveSince(since, onProgress?) – Archiv-Download via DMPAFT + */ + +import "dotenv/config"; +import { SerialPort } from "serialport"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { CRC } = require("crc-full"); + +// ── Einstellungen ────────────────────────────────────────────────────────── + +export const PORT_PATH = process.env.PORT_PATH ?? "/dev/ttyUSB0"; +export const BAUD_RATE = Number(process.env.BAUD_RATE ?? 19200); +const RAIN_CLICK = 0.2; // mm pro Klick (0,2 mm-Sammler) + +// ── CRC16-CCIT-ZERO (Davis-Protokoll) ───────────────────────────────────── + +const crc16 = CRC.default("CRC16_CCIT_ZERO"); +const makeCRC = (data) => { const v = crc16.compute(data); const b = Buffer.alloc(2); b.writeUInt16BE(v); return b; }; +const checkCRC = (buf) => crc16.compute(buf.slice(0, -2)) === buf.readUInt16BE(buf.length - 2); + +// ── Datum-Hilfsfunktionen ────────────────────────────────────────────────── + +function dateToWords(date) { + const word = ((date.getFullYear() - 2000) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(); + const time = date.getHours() * 100 + date.getMinutes(); + const buf = Buffer.alloc(4); + buf.writeUInt16LE(word, 0); + buf.writeUInt16LE(time, 2); + return buf; +} + +function parseDateTime(dateWord, timeWord) { + const year = ((dateWord >> 9) & 0x7F) + 2000; + const month = (dateWord >> 5) & 0x0F; + const day = dateWord & 0x1F; + return new Date(year, month - 1, day, Math.floor(timeWord / 100), timeWord % 100); +} + +// ── Einheitenumrechner (für Archiv- und LOOP-Rohdaten) ──────────────────── + +const fTenthToC = (raw) => raw === -32768 ? null : +((raw / 10 - 32) * 5 / 9).toFixed(1); +const mphToKmh = (raw) => raw === 255 ? null : +(raw * 1.60934).toFixed(1); +const inHgToHPa = (raw) => raw === 0 ? null : +(raw / 1000 * 33.8639).toFixed(1); + +// Windrichtung aus Archiv-Byte (0–15 Index) +const WIND_DIR = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"]; +const windDirStr = (raw) => raw === 255 ? null : (WIND_DIR[raw] ?? null); + +// Windrichtung aus LOOP1-Gradangabe (1–360, 0 = kein Wind) +const WIND_STEPS = [22.5,45,67.5,90,112.5,135,157.5,180,202.5,225,247.5,270,292.5,315,337.5,360]; +const WIND_ABBR = ["NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW","N"]; +const degToDir = (deg) => { + if (!deg) return null; + for (let i = 0; i < WIND_STEPS.length; i++) if (deg <= WIND_STEPS[i]) return WIND_ABBR[i]; + return "N"; +}; + +// ── Raw-Port-Helfer ──────────────────────────────────────────────────────── + +/** + * Gibt eine `readBytes(n, timeout)` Funktion zurück, die Bytes aus einem + * Raw-SerialPort akkumuliert und auflöst, sobald n Bytes angekommen sind + * oder der Timeout abgelaufen ist. + */ +export function makeReader(port) { + let buf = Buffer.alloc(0); + let waiter = null; + port.on("data", d => { + buf = Buffer.concat([buf, d]); + waiter?.(); + }); + return (n, timeout = 5000) => new Promise(res => { + const tryResolve = () => { + if (buf.length >= n) { + const chunk = buf.slice(0, n); + buf = buf.slice(n); + waiter = null; + res(chunk); + } + }; + const timer = setTimeout(() => { + waiter = null; + const chunk = buf; + buf = Buffer.alloc(0); + res(chunk); + }, timeout); + waiter = () => { if (buf.length >= n) { clearTimeout(timer); tryResolve(); } }; + tryResolve(); + }); +} + +// ── Wakeup (Raw-Port) ────────────────────────────────────────────────────── + +/** + * Weckt die Davis-Console auf einem Raw-SerialPort. + * Bis zu 3 Versuche à 1,2 s (jede zweite Wakeup-Anfrage bleibt ohne Antwort – + * normales Davis-Verhalten). + */ +export async function wakeUp(port, readBytes) { + for (let tries = 0; tries < 3; tries++) { + port.write("\n"); + const resp = await readBytes(2, 1200); + if (resp.length >= 2) return; + } + throw new Error("Console antwortet nicht auf Wakeup"); +} + +// ── Station (persistente Raw-Verbindung) ─────────────────────────────────── + +/** + * Öffnet eine persistente Raw-SerialPort-Verbindung zur Davis-Console. + * Gibt ein Station-Objekt zurück: { port, readBytes, disconnect } + */ +export async function connectStation() { + const port = new SerialPort({ path: PORT_PATH, baudRate: BAUD_RATE, autoOpen: false }); + await new Promise((res, rej) => port.open(err => err ? rej(err) : res())); + const readBytes = makeReader(port); + const disconnect = () => new Promise(res => setTimeout(() => port.close(res), 200)); + return { port, readBytes, disconnect }; +} + +/** + * Weckt die Davis-Console über das station.port / station.readBytes. + */ +export async function wakeUpStation(station) { + await wakeUp(station.port, station.readBytes); +} + +// ── LOOP1-Datensatz parsen ───────────────────────────────────────────────── +// +// Offsets im 99-Byte LOOP1-Paket (gemäß Davis Serial Communication Reference +// und vantjs-Quellcode parseLOOP1.js): +// +// 7 UINT16 LE Luftdruck (inHg × 1000), null wenn 0 +// 9 INT16 LE Innentemperatur (°F × 10), null wenn 32767 +// 11 UINT8 Innenluftfeuchte (%), null wenn 0 oder 255 +// 12 INT16 LE Außentemperatur (°F × 10), null wenn 32767 +// 14 UINT8 Windgeschwindigkeit (mph) +// 15 UINT8 Ø-Wind 10 min (mph) +// 16 UINT16 LE Windrichtung (Grad 1–360, 0 = kein Wind) +// 33 UINT8 Außenluftfeuchte (%), null wenn 0 oder 255 +// 41 UINT16 LE Regenrate (Klicks/h) +// 43 UINT8 UV-Index (× 0,1), null wenn 255 +// 44 INT16 LE Solarstrahlung (W/m²), null wenn 32767 +// 50 UINT16 LE Tagesregen (Klicks) +// 97-98: \n\r (Ende des Pakets) +// 97-98: CRC16 (letzte 2 Bytes) +// +// Antwort auf „LOOP 1\n": ACK (1 Byte 0x06) + 99 Byte Paket = 100 Byte gesamt + +function parseLOOP1(pkt) { + const temp = (raw) => raw === 32767 ? null : +((raw / 10 - 32) * 5 / 9).toFixed(1); + const hum = (raw) => (raw === 0 || raw === 255) ? null : raw; + const mph = (raw) => raw === 255 ? null : +(raw * 1.60934).toFixed(1); + const r1 = (v) => v !== null && v !== undefined ? +v.toFixed(1) : null; + + const press = pkt.readUInt16LE(7); + + return { + time: new Date(), + tempOut: temp(pkt.readInt16LE(12)), + tempOutHigh: null, + tempOutLow: null, + tempIn: temp(pkt.readInt16LE(9)), + humOut: hum(pkt[33]), + humIn: hum(pkt[11]), + windAvg: mph(pkt[14]), + windHigh: null, + windDir: degToDir(pkt.readUInt16LE(16)), + windHighDir: null, + pressure: press === 0 ? null : r1(press * 33.8639 / 1000), + rain: +(pkt.readUInt16LE(50) * RAIN_CLICK).toFixed(1), + rainRate: +(pkt.readUInt16LE(41) * RAIN_CLICK).toFixed(1), + solarRad: (pkt.readInt16LE(44) === 32767 || pkt.readInt16LE(44) < 0) + ? null : pkt.readInt16LE(44), + }; +} + +/** + * Liest einen LOOP1-Datensatz direkt über das serielle Protokoll. + * Ruft zuerst wakeUpStation auf. + */ +export async function fetchLoopData(station) { + await wakeUpStation(station); + + station.port.write("LOOP 1\n"); + const resp = await station.readBytes(100, 5000); + + if (resp.length < 100) throw new Error(`LOOP: zu kurz (${resp.length} Bytes)`); + if (resp[0] !== 0x06) throw new Error(`LOOP: kein ACK (0x${resp[0].toString(16)})`); + + const pkt = resp.slice(1); // 99-Byte-Paket + if (!checkCRC(pkt)) throw new Error("LOOP: CRC-Fehler"); + + return parseLOOP1(pkt); +} + +// ── Archiv-Datensatz parsen ──────────────────────────────────────────────── + +function parseRecord(buf) { + const dateWord = buf.readUInt16LE(0); + const timeWord = buf.readUInt16LE(2); + if (dateWord === 0xFFFF || dateWord === 0) return null; + return { + time: parseDateTime(dateWord, timeWord), + tempOut: fTenthToC(buf.readInt16LE(4)), + tempOutHigh: fTenthToC(buf.readInt16LE(6)), + tempOutLow: fTenthToC(buf.readInt16LE(8)), + tempIn: fTenthToC(buf.readInt16LE(20)), + humOut: buf[23] === 255 ? null : buf[23], + humIn: buf[22] === 255 ? null : buf[22], + windAvg: mphToKmh(buf[24]), + windHigh: mphToKmh(buf[25]), + windDir: windDirStr(buf[27]), + windHighDir: windDirStr(buf[26]), + pressure: inHgToHPa(buf.readUInt16LE(14)), + rain: +(buf.readUInt16LE(10) * RAIN_CLICK).toFixed(1), + rainRate: +(buf.readUInt16LE(12) * RAIN_CLICK).toFixed(1), + solarRad: buf.readUInt16LE(16) === 32767 ? null : buf.readUInt16LE(16), + }; +} + +// ── Archiv-Download (DMPAFT) ─────────────────────────────────────────────── + +/** + * Lädt alle seit `since` archivierten Datensätze von der Console herunter. + * + * @param {Date} since – Startzeitpunkt (exklusiv) + * @param {Function} onProgress – optionaler Callback(current, total) + * @returns {Promise} – Array von normalisierten Records + */ +export async function readArchiveSince(since, onProgress) { + const port = new SerialPort({ path: PORT_PATH, baudRate: BAUD_RATE, autoOpen: false }); + await new Promise((res, rej) => port.open(err => err ? rej(err) : res())); + + const readBytes = makeReader(port); + const records = []; + + try { + await wakeUp(port, readBytes); + + port.write("DMPAFT\n"); + const ack1 = await readBytes(1, 3000); + if (ack1[0] !== 0x06) throw new Error("Kein ACK nach DMPAFT: 0x" + ack1.toString("hex")); + + // 4-Byte-Datum + 2-Byte-CRC senden + const dtBuf = dateToWords(since); + port.write(Buffer.concat([dtBuf, makeCRC(dtBuf)])); + + // 7-Byte-Antwort-Header: ACK(1) + Seiten(2 LE) + ersterRecord(2 LE) + CRC(2) + const hdr = await readBytes(7, 5000); + if (hdr[0] !== 0x06) throw new Error("Kein ACK im Header"); + if (!checkCRC(hdr.slice(1))) throw new Error("Header CRC-Fehler"); + + const numPages = hdr.readUInt16LE(1); + const firstRec = hdr.readUInt16LE(3); + + if (numPages === 0) return records; + + port.write(Buffer.from([0x06])); // Übertragung starten + + for (let p = 0; p < numPages; p++) { + onProgress?.(p + 1, numPages); + + // 1 Seq-Byte + 5 × 52 Byte Records + 4 Byte Füller + 2 Byte CRC = 267 Byte + const page = await readBytes(267, 8000); + if (page.length < 267) throw new Error(`Seite ${p}: zu kurz (${page.length} Bytes)`); + if (!checkCRC(page)) throw new Error(`Seite ${p}: CRC-Fehler`); + + const startRec = (p === 0) ? firstRec : 0; + for (let r = startRec; r < 5; r++) { + const rec = parseRecord(page.slice(1 + r * 52, 1 + (r + 1) * 52)); + if (rec && rec.time > since) records.push(rec); + } + + // Nächste Seite anfordern; letzte Seite mit ESC abbrechen + port.write(Buffer.from([p < numPages - 1 ? 0x06 : 0x1B])); + } + } finally { + await new Promise(r => setTimeout(r, 300)); + port.close(); + } + + return records; +} diff --git a/db.js b/db.js new file mode 100644 index 0000000..b2a7bbb --- /dev/null +++ b/db.js @@ -0,0 +1,123 @@ +/** + * db.js – SQLite-Modul für Wetterdaten + * + * Exports: + * openDb(path) – DB öffnen / anlegen + * getLatestTs(db) – letzten archivierten Zeitstempel lesen + * insertRecord(db, record, source) – einzelnen Datensatz einfügen + * insertRecords(db, records, source) – Batch-Insert (Transaktion) + */ + +import Database from "better-sqlite3"; + +// ── Schema ───────────────────────────────────────────────────────────────── + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS readings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, -- Unix-Zeit in Sekunden (UTC) + source TEXT NOT NULL, -- 'archive' | 'loop' + temp_out REAL, -- °C + temp_out_high REAL, -- °C (nur Archiv) + temp_out_low REAL, -- °C (nur Archiv) + temp_in REAL, -- °C + hum_out INTEGER, -- % + hum_in INTEGER, -- % + wind_avg REAL, -- km/h + wind_high REAL, -- km/h (nur Archiv) + wind_dir TEXT, -- Himmelsrichtung + wind_high_dir TEXT, -- Himmelsrichtung (nur Archiv) + pressure REAL, -- hPa + rain REAL, -- mm (Archiv: Intervall; Loop: Tagessumme) + rain_rate REAL, -- mm/h + solar_rad INTEGER, -- W/m² + UNIQUE(ts, source) +); +CREATE INDEX IF NOT EXISTS idx_readings_ts ON readings(ts); +`; + +// ── Öffnen ───────────────────────────────────────────────────────────────── + +/** + * Öffnet (oder erstellt) die SQLite-Datenbank und initialisiert das Schema. + * @param {string} dbPath – Pfad zur .db-Datei + * @returns {Database} better-sqlite3 Instanz + */ +export function openDb(dbPath) { + const db = new Database(dbPath); + db.pragma("journal_mode = WAL"); + db.exec(SCHEMA); + return db; +} + +// ── Lesen ────────────────────────────────────────────────────────────────── + +/** + * Gibt den Unix-Zeitstempel (Sekunden) des neuesten Archiv-Eintrags zurück, + * oder null wenn die Tabelle leer ist. + */ +export function getLatestTs(db) { + const row = db.prepare( + "SELECT MAX(ts) AS ts FROM readings WHERE source = 'archive'" + ).get(); + return row?.ts ?? null; +} + +// ── Schreiben ────────────────────────────────────────────────────────────── + +const INSERT_SQL = ` +INSERT OR IGNORE INTO readings + (ts, source, temp_out, temp_out_high, temp_out_low, temp_in, + hum_out, hum_in, wind_avg, wind_high, wind_dir, wind_high_dir, + pressure, rain, rain_rate, solar_rad) +VALUES + (@ts, @source, @temp_out, @temp_out_high, @temp_out_low, @temp_in, + @hum_out, @hum_in, @wind_avg, @wind_high, @wind_dir, @wind_high_dir, + @pressure, @rain, @rain_rate, @solar_rad) +`; + +function toRow(record, source) { + return { + ts: Math.floor(record.time.getTime() / 1000), + source, + temp_out: record.tempOut ?? null, + temp_out_high: record.tempOutHigh ?? null, + temp_out_low: record.tempOutLow ?? null, + temp_in: record.tempIn ?? null, + hum_out: record.humOut ?? null, + hum_in: record.humIn ?? null, + wind_avg: record.windAvg ?? null, + wind_high: record.windHigh ?? null, + wind_dir: record.windDir ?? null, + wind_high_dir: record.windHighDir ?? null, + pressure: record.pressure ?? null, + rain: record.rain ?? null, + rain_rate: record.rainRate ?? null, + solar_rad: record.solarRad ?? null, + }; +} + +/** + * Fügt einen einzelnen Datensatz in die DB ein. + * Ignoriert Duplikate (gleiche ts + source) dank UNIQUE-Constraint. + */ +export function insertRecord(db, record, source) { + db.prepare(INSERT_SQL).run(toRow(record, source)); +} + +/** + * Fügt ein Array von Datensätzen in einer einzigen Transaktion ein. + * Gibt die Anzahl tatsächlich eingefügter Zeilen zurück. + */ +export function insertRecords(db, records, source) { + const stmt = db.prepare(INSERT_SQL); + const run = db.transaction(recs => { + let count = 0; + for (const r of recs) { + const info = stmt.run(toRow(r, source)); + count += info.changes; + } + return count; + }); + return run(records); +} diff --git a/loop.js b/loop.js new file mode 100644 index 0000000..9facb99 --- /dev/null +++ b/loop.js @@ -0,0 +1,30 @@ +import { connectStation, fetchLoopData } from "./davis.js"; + +let station = null; + +async function connect() { + station = await connectStation(); +} + +async function update() { + try { + const data = await fetchLoopData(station); + console.log( + `[${data.time.toLocaleTimeString("de-DE", { hour12: false })}] ` + + `Außen: ${data.tempOut?.toFixed(1)}°C ` + + `Feuchte: ${data.humOut}% ` + + `Wind: ${data.windAvg} km/h` + ); + } catch (e) { + console.error("Fehler:", e.message); + try { await station?.disconnect(); } catch {} + station = null; + try { await connect(); } catch {} + } + setTimeout(update, 30000); +} + +await connect(); +update(); + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..761b379 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,735 @@ +{ + "name": "wetter_1", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wetter_1", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "better-sqlite3": "^12.8.0", + "crc-full": "^1.1.0", + "dotenv": "^17.4.0", + "serialport": "^10.5.0" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz", + "integrity": "sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "^10.2.1", + "debug": "^4.3.2", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=12.17.0 <13.0 || >=14.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "license": "MIT", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz", + "integrity": "sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz", + "integrity": "sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz", + "integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz", + "integrity": "sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz", + "integrity": "sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw==", + "license": "MIT", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz", + "integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "10.5.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.5.0.tgz", + "integrity": "sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.5.0.tgz", + "integrity": "sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz", + "integrity": "sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz", + "integrity": "sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.5.0.tgz", + "integrity": "sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "^4.3.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/crc-full": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/crc-full/-/crc-full-1.1.0.tgz", + "integrity": "sha512-7YK4t8C9PiekOSnBotYjU2roaaorUXHyT+Xzb12Zgg4DsfG58AxmPk2/wx7XnC9UXyriqRvl3c+U0zFsZkdVYg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", + "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialport": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-10.5.0.tgz", + "integrity": "sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw==", + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "10.8.0", + "@serialport/parser-byte-length": "10.5.0", + "@serialport/parser-cctalk": "10.5.0", + "@serialport/parser-delimiter": "10.5.0", + "@serialport/parser-inter-byte-timeout": "10.5.0", + "@serialport/parser-packet-length": "10.5.0", + "@serialport/parser-readline": "10.5.0", + "@serialport/parser-ready": "10.5.0", + "@serialport/parser-regex": "10.5.0", + "@serialport/parser-slip-encoder": "10.5.0", + "@serialport/parser-spacepacket": "10.5.0", + "@serialport/stream": "10.5.0", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1fb5239 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "wetter_1", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "rxf", + "type": "module", + "main": "wetter.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "better-sqlite3": "^12.8.0", + "crc-full": "^1.1.0", + "dotenv": "^17.4.0", + "serialport": "^10.5.0" + } +} diff --git a/wetter.js b/wetter.js new file mode 100644 index 0000000..72bdf31 --- /dev/null +++ b/wetter.js @@ -0,0 +1,98 @@ +/** + * wetter.js – Hauptprogramm + * + * Ablauf beim Start: + * 1. SQLite-Datenbank öffnen (wetter.db) + * 2. Letzten archivierten Zeitstempel lesen → Archiv nachladen + * 3. Archivdaten in DB schreiben + * 4. LOOP-Schleife starten: alle 30 s Echtzeit-Daten holen & in DB schreiben + */ + +import "dotenv/config"; +import path from "path"; +import { fileURLToPath } from "url"; + +import { openDb, getLatestTs, insertRecords, insertRecord } from "./db.js"; +import { readArchiveSince, connectStation, fetchLoopData } from "./davis.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_PATH = process.env.DB_PATH ?? path.join(__dirname, "wetter.db"); +const LOOP_INTERVAL_MS = Number(process.env.LOOP_INTERVAL_MS ?? 30_000); + +// ── Hilfsfunktionen ──────────────────────────────────────────────────────── + +const fmt24h = (d) => d.toLocaleTimeString("de-DE", { hour12: false }); +const fmtDateTime = (d) => d.toLocaleString("de-DE", { hour12: false }); +function log(msg) { console.log (`[${fmt24h(new Date())}] ${msg}`); } +function warn(msg) { console.warn(`[${fmt24h(new Date())}] WARN ${msg}`); } +function err(msg) { console.error(`[${fmt24h(new Date())}] ERROR ${msg}`); } + +// ── Archiv nachladen ─────────────────────────────────────────────────────── + +async function catchUpArchive(db) { + const latestTs = getLatestTs(db); + const since = latestTs + ? new Date(latestTs * 1000) // ab letztem DB-Eintrag + : new Date(Date.now() - 24 * 60 * 60 * 1000); // Fallback: letzte 24 h + + log(`Lade Archiv ab ${fmtDateTime(since)} ...`); + + let lastPct = -1; + const records = await readArchiveSince(since, (cur, total) => { + const pct = Math.floor(cur / total * 100); + if (pct !== lastPct) { + process.stdout.write(`\r Archiv: ${pct}% (Seite ${cur}/${total})`); + lastPct = pct; + } + }); + process.stdout.write("\r\x1b[K"); // Fortschrittszeile löschen + + if (records.length === 0) { + log("Archiv: keine neuen Datensätze."); + return; + } + + const inserted = insertRecords(db, records, "archive"); + log(`Archiv: ${inserted} neue Datensätze gespeichert (${records.length} empfangen).`); +} + +// ── LOOP-Schleife ────────────────────────────────────────────────────────── + +async function runLoop(db) { + let station = null; + + async function connect() { + station = await connectStation(); + log("Verbunden mit Wetterstation."); + } + + async function tick() { + try { + const data = await fetchLoopData(station); + insertRecord(db, data, "loop"); + log( + `Außen: ${data.tempOut?.toFixed(1)}°C ` + + `Feuchte: ${data.humOut}% ` + + `Wind: ${data.windAvg} km/h ` + + `Druck: ${data.pressure} hPa` + ); + } catch (e) { + warn("LOOP-Fehler: " + e.message + " – Verbindung wird neu aufgebaut."); + try { await station?.disconnect(); } catch {} + station = null; + try { await connect(); } catch (ce) { err("Reconnect fehlgeschlagen: " + ce.message); } + } + setTimeout(tick, LOOP_INTERVAL_MS); + } + + await connect(); + tick(); +} + +// ── Hauptprogramm ────────────────────────────────────────────────────────── + +const db = openDb(DB_PATH); +log(`Datenbank: ${DB_PATH}`); + +await catchUpArchive(db); +await runLoop(db);