Berechnet SPEED aus Koordinaten (falls Zeitbasis ausreichend)

This commit is contained in:
2025-11-04 18:19:42 +01:00
parent 022dc097f3
commit d1660eed3b

View File

@@ -1,87 +1,127 @@
/*********************************************** /***********************************************
* EQA 300
* ioBroker → Traccar (OwnTracks über HTTP, Port 5144) * ioBroker → Traccar (OwnTracks über HTTP, Port 5144)
* sendet NUR bei Positionsänderung + mit Speed + Battery * - sendet NUR bei Positionsänderung
* DEBOUNCE: Lat/Lon werden zusammengefasst, um Tearing zu vermeiden * - berechnet SPEED aus Koordinaten (falls Zeitbasis ausreichend)
* - sendet Battery (SOC)
* - Debounce (leading-edge) gegen Lat/Lon-Tearing
***********************************************/ ***********************************************/
const http = require("http"); const http = require("http");
// === Grundkonfiguration === // === Grundkonfiguration ===
const TRACCAR_HOST = "192.168.2.125"; // IP oder Hostname deines Traccar const TRACCAR_HOST = "192.168.2.125"; // IP/Hostname Traccar
const TRACCAR_PORT = 5144; // OwnTracks-Port const TRACCAR_PORT = 5144; // OwnTracks-Port
const DEVICE_TID = "EQA"; // in Traccar muss das Gerät so heißen! const DEVICE_TID = "EQA"; // Einzigartige ID in Traccar
// === ioBroker-Datenpunkte === // === ioBroker-Datenpunkte ===
const LAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.positionLat.doubleValue"; const LAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.positionLat.doubleValue";
const LON_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.positionLong.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"; // String "69" → in Zahl casten
const BAT_STATE = "mercedesme.0.W1N9N0KB9TJ192372.state.soc.displayValue";
// === Sende- und Bewegungsparameter === // === Parameter ===
const SEND_INTERVAL_SECONDS = 60; // periodische Sicherung const SEND_INTERVAL_SECONDS = 60; // Fallback/periodische Sicherung
const MIN_DISTANCE_METERS = 5; // minimale Bewegung const MIN_DISTANCE_METERS = 5; // min. Bewegung
const DEBOUNCE_MS = 700; // Wartezeit, um Lat/Lon gemeinsam zu lesen const DEBOUNCE_MS = 700; // bündelt Lat+Lon
const MAX_TS_SKEW_MS = 2000; // max. Zeitdifferenz zwischen Lat- und Lon-ts 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
// interne Speicherung der letzten gesendeten Position const ENABLE_SPEED_SMOOTHING = true;
let lastLat = null; const SPEED_EMA_ALPHA = 0.5; // 0..1
let lastLon = null;
// Debounce-Handling // === Zustände ===
let debounceTimer = null; let lastFix = null; // { lat, lon, tsMs }
let emaSpeed = null; // geglättete Geschwindigkeit
let debounceTimer = null; // Timer-ID
let lastTriggerMs = 0; // Zeit des letzten Triggers
// Distanzberechnung (Haversine) // --- Utils ---
function distanceMeters(lat1, lon1, lat2, lon2) { function distanceMeters(lat1, lon1, lat2, lon2) {
const R = 6371000; 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), dLon = toRad(lon2 - lon1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat/2)**2 + const a = 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));
} }
function readNumeric(id) { function readNumeric(id) {
const s = getState(id); const s = getState(id);
if (!s) return { val: null, ts: 0 }; if (!s) return { val: null, ts: 0 };
const v = (s.val === null || s.val === undefined) ? null : Number(s.val); const v = (s.val == null) ? null : Number(s.val);
return { val: Number.isFinite(v) ? v : null, ts: s.ts || 0 }; return { val: Number.isFinite(v) ? v : null, ts: s.ts || 0 };
} }
// sendet **ohne** Debounce mit den aktuellsten, zusammengehörigen Werten // --- 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) { function sendOwntracks(force = false) {
const { val: lat, ts: tsLat } = readNumeric(LAT_STATE); const { val: lat, ts: tsLat } = readNumeric(LAT_STATE);
const { val: lon, ts: tsLon } = readNumeric(LON_STATE); const { val: lon, ts: tsLon } = readNumeric(LON_STATE);
const { val: speed } = readNumeric(SPD_STATE); const { val: rawBatt } = readNumeric(BAT_STATE);
const { val: batt } = readNumeric(BAT_STATE); const batt = (rawBatt != null) ? Number(rawBatt) : NaN;
if (lat == null || lon == null) { if (lat == null || lon == null) {
log("OwnTracks→Traccar: ungültige Koordinaten sende nicht", "warn"); // ungültige Koordinaten → nichts senden
return; return;
} }
// Prüfe, ob Lat/Lon zeitlich zusammen gehören // Lat/Lon müssen zeitlich zusammenpassen
const skew = Math.abs(tsLat - tsLon); const skew = Math.abs(tsLat - tsLon);
if (!force && skew > MAX_TS_SKEW_MS) { if (!force && skew > MAX_TS_SKEW_MS) {
// Lat/Lon kamen zeitversetzt rein → nochmal kurz warten/sammeln // Zu großer Zeitversatz → später nochmal, aber NICHT endlos neu timern
log(`OwnTracks→Traccar: Lat/Lon ts-skew ${skew}ms > ${MAX_TS_SKEW_MS}ms sende später.`, "info"); scheduleDebounced();
scheduleDebounced(); // neu sammeln
return; return;
} }
// Bewegung checken const fixTsMs = Math.max(tsLat, tsLon);
if (!force && lastLat !== null && lastLon !== null) {
const dist = distanceMeters(lastLat, lastLon, lat, lon); // Distanz und (optional) Speed
if (dist < MIN_DISTANCE_METERS) { let speedKmh = NaN;
log(`OwnTracks→Traccar: keine relevante Bewegung (${dist.toFixed(1)} m) nichts gesendet.`); if (lastFix) {
const dist = distanceMeters(lastFix.lat, lastFix.lon, lat, lon);
if (!force && dist < MIN_DISTANCE_METERS) {
// keine relevante Bewegung → nichts senden
return; return;
} }
// Optionaler Ausreißer-Filter: Distanz absurd groß? (z.B. >5 km seit letztem Punkt)
if (dist > 5000) { // Zeitbasis für Speed
log(`OwnTracks→Traccar: Ausreißer verworfen (${dist.toFixed(0)} m).`, "warn"); const dt = Math.max((fixTsMs - lastFix.tsMs) / 1000, 0);
return;
// 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 tst = Math.floor(Date.now() / 1000);
@@ -94,12 +134,12 @@ function sendOwntracks(force = false) {
"tst": tst, "tst": tst,
"acc": 5 "acc": 5
}; };
if (Number.isFinite(speed)) data.vel = speed; if (Number.isFinite(speedKmh)) data.vel = Math.round(speedKmh);
if (Number.isFinite(batt)) data.batt = batt; if (Number.isFinite(batt)) data.batt = batt;
const payload = JSON.stringify(data); const payload = JSON.stringify(data);
const options = { const req = http.request({
host: TRACCAR_HOST, host: TRACCAR_HOST,
port: TRACCAR_PORT, port: TRACCAR_PORT,
path: "/", path: "/",
@@ -109,45 +149,25 @@ function sendOwntracks(force = false) {
"Content-Length": Buffer.byteLength(payload), "Content-Length": Buffer.byteLength(payload),
"Connection": "close" "Connection": "close"
} }
}; }, (res) => {
// Body ist nicht relevant; Update lastFix nach (auch bei 204)
const req = http.request(options, res => { res.resume();
let body = ""; lastFix = { lat, lon, tsMs: fixTsMs };
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.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 // --- Intervall + Trigger ---
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); setInterval(() => sendOwntracks(false), SEND_INTERVAL_SECONDS * 1000);
// bei Änderung von Lat ODER Lon nur debouncen (nicht sofort senden) // Bei Änderung von Lat ODER Lon: nur debouncen (Timer NICHT resetten)
on({ id: LAT_STATE, change: "ne" }, scheduleDebounced); on({ id: LAT_STATE, change: "ne" }, scheduleDebounced);
on({ id: LON_STATE, change: "ne" }, scheduleDebounced); on({ id: LON_STATE, change: "ne" }, scheduleDebounced);
// erstes Mal sofort senden // Start mit force
sendOwntracks(true); 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).`); 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).`);