From d1660eed3b24863be5c54a20ea5bf35e5636c251 Mon Sep 17 00:00:00 2001 From: Michael Scheidel Date: Tue, 4 Nov 2025 18:19:42 +0100 Subject: [PATCH] Berechnet SPEED aus Koordinaten (falls Zeitbasis ausreichend) --- java-scripts/Traccar Logging: EQA 300.js | 166 +++++++++++++---------- 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/java-scripts/Traccar Logging: EQA 300.js b/java-scripts/Traccar Logging: EQA 300.js index 10a0653..04b6e2b 100644 --- a/java-scripts/Traccar Logging: EQA 300.js +++ b/java-scripts/Traccar Logging: EQA 300.js @@ -1,87 +1,127 @@ /*********************************************** - * EQA 300 * ioBroker → Traccar (OwnTracks über HTTP, Port 5144) - * sendet NUR bei Positionsänderung + mit Speed + Battery - * DEBOUNCE: Lat/Lon werden zusammengefasst, um Tearing zu vermeiden + * - sendet NUR bei Positionsänderung + * - berechnet SPEED aus Koordinaten (falls Zeitbasis ausreichend) + * - sendet Battery (SOC) + * - Debounce (leading-edge) gegen Lat/Lon-Tearing ***********************************************/ const http = require("http"); // === Grundkonfiguration === -const TRACCAR_HOST = "192.168.2.125"; // IP oder Hostname deines Traccar +const TRACCAR_HOST = "192.168.2.125"; // IP/Hostname Traccar const TRACCAR_PORT = 5144; // OwnTracks-Port -const DEVICE_TID = "EQA"; // in Traccar muss das Gerät so heißen! +const DEVICE_TID = "EQA"; // Einzigartige ID in Traccar // === ioBroker-Datenpunkte === const LAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.positionLat.doubleValue"; const LON_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.positionLong.doubleValue"; -const SPD_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.speedUnitFromIC.doubleValue"; -const BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue"; +const BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue"; // String "69" → in Zahl casten -// === Sende- und Bewegungsparameter === -const SEND_INTERVAL_SECONDS = 60; // periodische Sicherung -const MIN_DISTANCE_METERS = 5; // minimale Bewegung -const DEBOUNCE_MS = 700; // Wartezeit, um Lat/Lon gemeinsam zu lesen -const MAX_TS_SKEW_MS = 2000; // max. Zeitdifferenz zwischen Lat- und Lon-ts +// === Parameter === +const SEND_INTERVAL_SECONDS = 60; // Fallback/periodische Sicherung +const MIN_DISTANCE_METERS = 5; // min. Bewegung +const DEBOUNCE_MS = 700; // bündelt Lat+Lon +const MAX_TS_SKEW_MS = 2000; // max. Zeitversatz Lat/Lon +const MAX_SPEED_KMH = 250; // Speed-Ausreißer +const MIN_DT_SECONDS = 1.0; // min. Zeitabstand für SPEED +const MAX_WAIT_AFTER_TRIGGER_MS = 2000; // hartes Auslösen nach X ms -// interne Speicherung der letzten gesendeten Position -let lastLat = null; -let lastLon = null; +const ENABLE_SPEED_SMOOTHING = true; +const SPEED_EMA_ALPHA = 0.5; // 0..1 -// Debounce-Handling -let debounceTimer = null; +// === Zustände === +let lastFix = null; // { lat, lon, tsMs } +let emaSpeed = null; // geglättete Geschwindigkeit +let debounceTimer = null; // Timer-ID +let lastTriggerMs = 0; // Zeit des letzten Triggers -// Distanzberechnung (Haversine) +// --- Utils --- function distanceMeters(lat1, lon1, lat2, lon2) { const R = 6371000; const toRad = v => v * Math.PI / 180; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); + const dLat = toRad(lat2 - lat1), dLon = toRad(lon2 - lon1); const a = Math.sin(dLat/2)**2 + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * - Math.sin(dLon/2)**2; + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } - function readNumeric(id) { const s = getState(id); if (!s) return { val: null, ts: 0 }; - const v = (s.val === null || s.val === undefined) ? null : Number(s.val); + const v = (s.val == null) ? null : Number(s.val); return { val: Number.isFinite(v) ? v : null, ts: s.ts || 0 }; } -// sendet – **ohne** Debounce – mit den aktuellsten, zusammengehörigen Werten +// --- Debounce leading-edge --- +function scheduleDebounced() { + const now = Date.now(); + lastTriggerMs = now; + if (debounceTimer) return; // WICHTIG: Timer läuft schon → NICHT neu setzen + + debounceTimer = setTimeout(() => { + debounceTimer = null; + sendOwntracks(false); + }, DEBOUNCE_MS); + + // Watchdog: falls Events fluten, trotzdem spätestens nach X ms senden + setTimeout(() => { + if (debounceTimer && Date.now() - lastTriggerMs >= MAX_WAIT_AFTER_TRIGGER_MS) { + clearTimeout(debounceTimer); + debounceTimer = null; + sendOwntracks(false); + } + }, MAX_WAIT_AFTER_TRIGGER_MS + 50); +} + +// --- Hauptlogik --- function sendOwntracks(force = false) { const { val: lat, ts: tsLat } = readNumeric(LAT_STATE); const { val: lon, ts: tsLon } = readNumeric(LON_STATE); - const { val: speed } = readNumeric(SPD_STATE); - const { val: batt } = readNumeric(BAT_STATE); + const { val: rawBatt } = readNumeric(BAT_STATE); + const batt = (rawBatt != null) ? Number(rawBatt) : NaN; if (lat == null || lon == null) { - log("OwnTracks→Traccar: ungültige Koordinaten – sende nicht", "warn"); + // ungültige Koordinaten → nichts senden return; } - // Prüfe, ob Lat/Lon zeitlich „zusammen gehören“ + // Lat/Lon müssen zeitlich zusammenpassen const skew = Math.abs(tsLat - tsLon); if (!force && skew > MAX_TS_SKEW_MS) { - // Lat/Lon kamen zeitversetzt rein → nochmal kurz warten/sammeln - log(`OwnTracks→Traccar: Lat/Lon ts-skew ${skew}ms > ${MAX_TS_SKEW_MS}ms – sende später.`, "info"); - scheduleDebounced(); // neu sammeln + // Zu großer Zeitversatz → später nochmal, aber NICHT endlos neu timern + scheduleDebounced(); return; } - // Bewegung checken - if (!force && lastLat !== null && lastLon !== null) { - const dist = distanceMeters(lastLat, lastLon, lat, lon); - if (dist < MIN_DISTANCE_METERS) { - log(`OwnTracks→Traccar: keine relevante Bewegung (${dist.toFixed(1)} m) – nichts gesendet.`); + const fixTsMs = Math.max(tsLat, tsLon); + + // Distanz und (optional) Speed + let speedKmh = NaN; + if (lastFix) { + const dist = distanceMeters(lastFix.lat, lastFix.lon, lat, lon); + if (!force && dist < MIN_DISTANCE_METERS) { + // keine relevante Bewegung → nichts senden return; } - // Optionaler Ausreißer-Filter: Distanz absurd groß? (z.B. >5 km seit letztem Punkt) - if (dist > 5000) { - log(`OwnTracks→Traccar: Ausreißer verworfen (${dist.toFixed(0)} m).`, "warn"); - return; + + // Zeitbasis für Speed + const dt = Math.max((fixTsMs - lastFix.tsMs) / 1000, 0); + + // Falls dt zu klein: wir SENDEN trotzdem (ohne vel) – KEIN neues Debounce! + if (dt >= MIN_DT_SECONDS) { + // Ausreißer-Schutz + if (dist > 5000 && dt < 5) return; + + let v = (dist / dt) * 3.6; + if (v > MAX_SPEED_KMH) return; + + if (ENABLE_SPEED_SMOOTHING) { + emaSpeed = (emaSpeed == null) ? v : (SPEED_EMA_ALPHA * v + (1 - SPEED_EMA_ALPHA) * emaSpeed); + v = emaSpeed; + } + speedKmh = v; } + } else { + // first fix: immer senden (ohne vel) } const tst = Math.floor(Date.now() / 1000); @@ -94,12 +134,12 @@ function sendOwntracks(force = false) { "tst": tst, "acc": 5 }; - if (Number.isFinite(speed)) data.vel = speed; - if (Number.isFinite(batt)) data.batt = batt; + if (Number.isFinite(speedKmh)) data.vel = Math.round(speedKmh); + if (Number.isFinite(batt)) data.batt = batt; const payload = JSON.stringify(data); - const options = { + const req = http.request({ host: TRACCAR_HOST, port: TRACCAR_PORT, path: "/", @@ -109,45 +149,25 @@ function sendOwntracks(force = false) { "Content-Length": Buffer.byteLength(payload), "Connection": "close" } - }; - - const req = http.request(options, res => { - let body = ""; - res.on("data", chunk => body += chunk); - res.on("end", () => { - log(`OwnTracks→Traccar: gesendet lat=${lat}, lon=${lon}, status=${res.statusCode}, vel=${Number.isFinite(speed)?speed:"-"}, batt=${Number.isFinite(batt)?batt:"-"}`); - if (body && body.trim()) log("OwnTracks→Traccar: Antwort: " + body); - lastLat = lat; - lastLon = lon; - }); + }, (res) => { + // Body ist nicht relevant; Update lastFix nach (auch bei 204) + res.resume(); + lastFix = { lat, lon, tsMs: fixTsMs }; }); - req.on("error", err => log("OwnTracks→Traccar: HTTP-Fehler: " + err, "error")); + req.on("error", (err) => log("OwnTracks→Traccar: HTTP-Fehler: " + err, "error")); req.write(payload); req.end(); } -// Debounce-Planer: sammelt Lat+Lon kurz, bevor gesendet wird -function scheduleDebounced() { - if (debounceTimer) { - clearTimeout(debounceTimer); - debounceTimer = null; - } - debounceTimer = setTimeout(() => { - debounceTimer = null; - sendOwntracks(false); - }, DEBOUNCE_MS); -} - -// === Intervall + Trigger === -// periodischer Sicherungs-Check (falls mal kein Trigger käme) +// --- Intervall + Trigger --- setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000); -// bei Änderung von Lat ODER Lon nur debouncen (nicht sofort senden) +// Bei Änderung von Lat ODER Lon: nur debouncen (Timer NICHT resetten) on({ id: LAT_STATE, change: "ne" }, scheduleDebounced); on({ id: LON_STATE, change: "ne" }, scheduleDebounced); -// erstes Mal sofort senden +// Start mit force sendOwntracks(true); -log(`OwnTracks→Traccar-Sender gestartet (Debounce ${DEBOUNCE_MS}ms, max ts-skew ${MAX_TS_SKEW_MS}ms, Δdist ≥ ${MIN_DISTANCE_METERS}m, Intervall ${SEND_INTERVAL_SECONDS}s).`); +log(`OwnTracks→Traccar gestartet (Debounce ${DEBOUNCE_MS}ms, max skew ${MAX_TS_SKEW_MS}ms, Δdist ≥ ${MIN_DISTANCE_METERS}m, Intervall ${SEND_INTERVAL_SECONDS}s).`);