/*********************************************** * E 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 ***********************************************/ const http = require("http"); // === Grundkonfiguration === const TRACCAR_HOST = "192.168.2.125"; // IP oder Hostname deines Traccar const TRACCAR_PORT = 5144; // OwnTracks-Port const DEVICE_TID = "EC"; // in Traccar muss das Gerät so heißen! // === ioBroker-Datenpunkte === const LAT_STATE = "mercedesme.0.W1KLH0JB9SA165764.state.positionLat.doubleValue"; const LON_STATE = "mercedesme.0.W1KLH0JB9SA165764.state.positionLong.doubleValue"; const SPD_STATE = "mercedesme.0.W1KLH0JB9SA165764.state.speedUnitFromIC.doubleValue"; const BAT_STATE = "mercedesme.0.W1KLH0JB9SA165764.state.soc.displayValue"; // === 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 // interne Speicherung der letzten gesendeten Position let lastLat = null; let lastLon = null; // Debounce-Handling let debounceTimer = null; // Distanzberechnung (Haversine) 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 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 || 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 { 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); if (lat == null || lon == null) { log("OwnTracks→Traccar: ungültige Koordinaten – sende nicht", "warn"); return; } // 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; } // 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; } // 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 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; 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 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(); } // 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) setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000); // 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 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).`);