Files
homelab-iobroker-scripts/java-scripts/Traccar Logging: EQA 300.js

174 lines
5.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/***********************************************
* 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).`);