diff --git a/java-scripts/Traccar Logging: EQA 300.js b/java-scripts/Traccar Logging: EQA 300.js index 64d84a6..16a69c1 100644 --- a/java-scripts/Traccar Logging: EQA 300.js +++ b/java-scripts/Traccar Logging: EQA 300.js @@ -1,7 +1,8 @@ /*********************************************** - * EQA 300 ioBroker → Traccar (OwnTracks über HTTP, Port 5144) + * EQA 300 + * ioBroker → Traccar (OwnTracks über HTTP, Port 5144) * sendet NUR bei Positionsänderung + mit Speed + Battery - * jetzt mit setInterval() statt schedule() + * DEBOUNCE: Lat/Lon werden zusammengefasst, um Tearing zu vermeiden ***********************************************/ const http = require("http"); @@ -17,105 +18,136 @@ const SPD_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.speedUnitFromIC.doubleVa const BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue"; // === Sende- und Bewegungsparameter === -const SEND_INTERVAL_SECONDS = 180; // wie oft prüfen (hier: alle 3 Minuten) -const MIN_DISTANCE_METERS = 15; // minimale Bewegung für neue Übertragung +const SEND_INTERVAL_SECONDS = 180; // periodische Sicherung +const MIN_DISTANCE_METERS = 15; // 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 -// interne Speicherung der letzten Position +// interne Speicherung der letzten gesendeten Position let lastLat = null; let lastLon = null; -// === Hilfsfunktion: Haversine-Distanz (Meter) === +// Debounce-Handling +let debounceTimer = null; + +// Distanzberechnung (Haversine) function distanceMeters(lat1, lon1, lat2, lon2) { - const R = 6371000; // Erdradius in Metern - const toRad = (v) => v * Math.PI / 180; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); - const a = - Math.sin(dLat / 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)); + const R = 6371000; + const toRad = v => v * Math.PI / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat/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)); } -// === Hauptfunktion === +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); + return { val: Number.isFinite(v) ? v : null, ts: s.ts || 0 }; +} + +// sendet – **ohne** Debounce – mit den aktuellsten, zusammengehörigen Werten function sendOwntracks(force = false) { - const lat = getState(LAT_STATE)?.val; - const lon = getState(LON_STATE)?.val; + 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 rawSpeed = getState(SPD_STATE)?.val; - const rawBatt = getState(BAT_STATE)?.val; + if (lat == null || lon == null) { + log("OwnTracks→Traccar: ungültige Koordinaten – sende nicht", "warn"); + return; + } - const speed = rawSpeed !== undefined && rawSpeed !== null ? Number(rawSpeed) : NaN; - const batt = rawBatt !== undefined && rawBatt !== null ? Number(rawBatt) : NaN; + // Prüfe, ob Lat/Lon zeitlich „zusammen gehören“ + 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 + return; + } - if (lat == null || lon == null) { - log("OwnTracks→Traccar: keine gültigen Koordinaten – sende nicht", "warn"); - 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.`); + return; } - - // Bewegung prüfen - 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.`); - 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; } + } - const tst = Math.floor(Date.now() / 1000); + const tst = Math.floor(Date.now() / 1000); - // === OwnTracks-kompatibles JSON === - const data = { - "_type": "location", - "lat": lat, - "lon": lon, - "tid": DEVICE_TID, - "tst": tst, - "acc": 5 - }; + const data = { + "_type": "location", + "lat": lat, + "lon": lon, + "tid": DEVICE_TID, + "tst": tst, + "acc": 5 + }; + if (Number.isFinite(speed)) data.vel = speed; + if (Number.isFinite(batt)) data.batt = batt; - if (!Number.isNaN(speed)) data.vel = speed; - if (!Number.isNaN(batt)) data.batt = batt; + const payload = JSON.stringify(data); - const payload = JSON.stringify(data); + const options = { + host: TRACCAR_HOST, + port: TRACCAR_PORT, + path: "/", + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(payload), + "Connection": "close" + } + }; - const options = { - host: TRACCAR_HOST, - port: TRACCAR_PORT, - path: "/", - method: "POST", - headers: { - "Content-Type": "application/json", - "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}, speed=${!Number.isNaN(speed)?speed:"-"}, batt=${!Number.isNaN(batt)?batt:"-"}`); - if (body && body.trim()) log("OwnTracks→Traccar: Antwort: " + body); - lastLat = lat; - lastLon = lon; - }); + 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; }); + }); - req.on("error", (err) => log("OwnTracks→Traccar: HTTP-Fehler: " + err, "error")); - req.write(payload); - req.end(); + 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 === -// → läuft alle SEND_INTERVAL_SECONDS Sekunden +// periodischer Sicherungs-Check (falls mal kein Trigger käme) setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000); -// → sofortige Übertragung bei Koordinatenänderung -on({ id: LAT_STATE, change: "ne" }, () => sendOwntracks(false)); -on({ id: LON_STATE, change: "ne" }, () => sendOwntracks(false)); +// bei Änderung von Lat ODER Lon nur debouncen (nicht sofort senden) +on({ id: LAT_STATE, change: "ne" }, scheduleDebounced); +on({ id: LON_STATE, change: "ne" }, scheduleDebounced); -// → erstes Mal sofort senden +// erstes Mal sofort senden sendOwntracks(true); -log(`OwnTracks→Traccar-Sender gestartet (alle ${SEND_INTERVAL_SECONDS}s, nur bei Bewegung, mit Speed & Battery).`); +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).`);