154 lines
5.3 KiB
JavaScript
154 lines
5.3 KiB
JavaScript
/***********************************************
|
||
* 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
|
||
***********************************************/
|
||
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 = "EQA"; // in Traccar muss das Gerät so heißen!
|
||
|
||
// === 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";
|
||
|
||
// === Sende- und Bewegungsparameter ===
|
||
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 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).`);
|