174 lines
5.6 KiB
JavaScript
174 lines
5.6 KiB
JavaScript
/***********************************************
|
||
* 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).`);
|