java-scripts/Traccar Logging: EQA 300.js aktualisiert

This commit is contained in:
2025-11-04 15:56:28 +01:00
parent bce5604d70
commit 4745e412fe

View File

@@ -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 * 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"); 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"; const BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue";
// === Sende- und Bewegungsparameter === // === Sende- und Bewegungsparameter ===
const SEND_INTERVAL_SECONDS = 180; // wie oft prüfen (hier: alle 3 Minuten) const SEND_INTERVAL_SECONDS = 180; // periodische Sicherung
const MIN_DISTANCE_METERS = 15; // minimale Bewegung für neue Übertragung 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 lastLat = null;
let lastLon = null; let lastLon = null;
// === Hilfsfunktion: Haversine-Distanz (Meter) === // Debounce-Handling
let debounceTimer = null;
// Distanzberechnung (Haversine)
function distanceMeters(lat1, lon1, lat2, lon2) { function distanceMeters(lat1, lon1, lat2, lon2) {
const R = 6371000; // Erdradius in Metern const R = 6371000;
const toRad = (v) => v * Math.PI / 180; const toRad = v => v * Math.PI / 180;
const dLat = toRad(lat2 - lat1); const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1); const dLon = toRad(lon2 - lon1);
const a = const a = Math.sin(dLat/2)**2 +
Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2;
Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
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) { function sendOwntracks(force = false) {
const lat = getState(LAT_STATE)?.val; const { val: lat, ts: tsLat } = readNumeric(LAT_STATE);
const lon = getState(LON_STATE)?.val; 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; if (lat == null || lon == null) {
const rawBatt = getState(BAT_STATE)?.val; log("OwnTracks→Traccar: ungültige Koordinaten sende nicht", "warn");
return;
}
const speed = rawSpeed !== undefined && rawSpeed !== null ? Number(rawSpeed) : NaN; // Prüfe, ob Lat/Lon zeitlich „zusammen gehören“
const batt = rawBatt !== undefined && rawBatt !== null ? Number(rawBatt) : NaN; 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) { // Bewegung checken
log("OwnTracks→Traccar: keine gültigen Koordinaten sende nicht", "warn"); if (!force && lastLat !== null && lastLon !== null) {
return; 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)
// Bewegung prüfen if (dist > 5000) {
if (!force && lastLat !== null && lastLon !== null) { log(`OwnTracks→Traccar: Ausreißer verworfen (${dist.toFixed(0)} m).`, "warn");
const dist = distanceMeters(lastLat, lastLon, lat, lon); return;
if (dist < MIN_DISTANCE_METERS) {
log(`OwnTracks→Traccar: keine relevante Bewegung (${dist.toFixed(1)} m) nichts gesendet.`);
return;
}
} }
}
const tst = Math.floor(Date.now() / 1000); const tst = Math.floor(Date.now() / 1000);
// === OwnTracks-kompatibles JSON === const data = {
const data = { "_type": "location",
"_type": "location", "lat": lat,
"lat": lat, "lon": lon,
"lon": lon, "tid": DEVICE_TID,
"tid": DEVICE_TID, "tst": tst,
"tst": tst, "acc": 5
"acc": 5 };
}; if (Number.isFinite(speed)) data.vel = speed;
if (Number.isFinite(batt)) data.batt = batt;
if (!Number.isNaN(speed)) data.vel = speed; const payload = JSON.stringify(data);
if (!Number.isNaN(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 options = { const req = http.request(options, res => {
host: TRACCAR_HOST, let body = "";
port: TRACCAR_PORT, res.on("data", chunk => body += chunk);
path: "/", res.on("end", () => {
method: "POST", log(`OwnTracks→Traccar: gesendet lat=${lat}, lon=${lon}, status=${res.statusCode}, vel=${Number.isFinite(speed)?speed:"-"}, batt=${Number.isFinite(batt)?batt:"-"}`);
headers: { if (body && body.trim()) log("OwnTracks→Traccar: Antwort: " + body);
"Content-Type": "application/json", lastLat = lat;
"Content-Length": Buffer.byteLength(payload), lastLon = lon;
"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;
});
}); });
});
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.write(payload);
req.end(); 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 === // === 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); setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000);
// → sofortige Übertragung bei Koordinatenänderung // bei Änderung von Lat ODER Lon nur debouncen (nicht sofort senden)
on({ id: LAT_STATE, change: "ne" }, () => sendOwntracks(false)); on({ id: LAT_STATE, change: "ne" }, scheduleDebounced);
on({ id: LON_STATE, change: "ne" }, () => sendOwntracks(false)); on({ id: LON_STATE, change: "ne" }, scheduleDebounced);
// erstes Mal sofort senden // erstes Mal sofort senden
sendOwntracks(true); 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).`);