/*********************************************** * ioBroker → Traccar (OwnTracks über HTTP, Port 5144) * - 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/Hostname Traccar const TRACCAR_PORT = 5144; // OwnTracks-Port 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 BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue"; // String "69" → in Zahl casten // === 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 const ENABLE_SPEED_SMOOTHING = true; const SPEED_EMA_ALPHA = 0.5; // 0..1 // === 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 // --- Utils --- function distanceMeters(lat1, lon1, lat2, lon2) { const R = 6371000; const toRad = v => v * Math.PI / 180; 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; 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) ? null : Number(s.val); return { val: Number.isFinite(v) ? v : null, ts: s.ts || 0 }; } // --- 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: rawBatt } = readNumeric(BAT_STATE); const batt = (rawBatt != null) ? Number(rawBatt) : NaN; if (lat == null || lon == null) { // ungültige Koordinaten → nichts senden return; } // Lat/Lon müssen zeitlich zusammenpassen const skew = Math.abs(tsLat - tsLon); if (!force && skew > MAX_TS_SKEW_MS) { // Zu großer Zeitversatz → später nochmal, aber NICHT endlos neu timern scheduleDebounced(); return; } 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; } // 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); const data = { "_type": "location", "lat": lat, "lon": lon, "tid": DEVICE_TID, "tst": tst, "acc": 5 }; if (Number.isFinite(speedKmh)) data.vel = Math.round(speedKmh); if (Number.isFinite(batt)) data.batt = batt; const payload = JSON.stringify(data); const req = http.request({ host: TRACCAR_HOST, port: TRACCAR_PORT, path: "/", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload), "Connection": "close" } }, (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.write(payload); req.end(); } // --- Intervall + Trigger --- setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000); // 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); // Start mit forces sendOwntracks(true); 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).`);