First commit
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal 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
13
.env
Normal 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
13
.gitignore
vendored
Normal 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
29
.vscode/launch.json
vendored
Normal 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
33
Dockerfile
Normal 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
37
archive.js
Normal 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
23
compose.yaml
Normal 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
298
davis.js
Normal 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 (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>} – 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
123
db.js
Normal 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
30
loop.js
Normal 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
735
package-lock.json
generated
Normal 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
18
package.json
Normal 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
98
wetter.js
Normal 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);
|
||||||
Reference in New Issue
Block a user