First commit

This commit is contained in:
2026-04-03 22:24:22 +02:00
commit d1cfee0dea
13 changed files with 1464 additions and 0 deletions

14
.dockerignore Normal file
View File

@@ -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

13
.env Normal file
View File

@@ -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

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Abhängigkeiten
node_modules/
# Lokale Konfiguration
.env.local
# Datenbank
*.db
*.db-shm
*.db-wal
# Logs
npm-debug.log*

29
.vscode/launch.json vendored Normal file
View File

@@ -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": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/wetter.js"
},
{
"type": "node",
"request": "launch",
"name": "Launch archive",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/archive.js"
}
]
}

33
Dockerfile Normal file
View File

@@ -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"]

37
archive.js Normal file
View File

@@ -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));

23
compose.yaml Normal file
View File

@@ -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:

298
davis.js Normal file
View File

@@ -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 (015 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 (1360, 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 1360, 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>} 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;
}

123
db.js Normal file
View File

@@ -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);
}

30
loop.js Normal file
View File

@@ -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();

735
package-lock.json generated Normal file
View File

@@ -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"
}
}
}

18
package.json Normal file
View File

@@ -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"
}
}

98
wetter.js Normal file
View File

@@ -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);