Batteriespannung: 8V-Plausibilitätsgrenze; Fahrten: echte Start-/Zielposition und Streckenlinie
Batteriespannung: Werte unter 8V (Sensor-/Verbindungsfehler, nicht physikalisch
plausibel für eine 12V-Bleibatterie) werden nicht mehr erfasst - live und beim
Import. "Neue Räder anlegen"-Knopf sitzt jetzt auf derselben Zeile wie "Montiert".
Fahrten: start_lat/-lon, end_lat/-lon und route wurden bisher nur beim
rückwirkenden Import befüllt, nie bei live erkannten Fahrten (dem Normalfall) -
Screening trägt sie jetzt aus dem GPS-Verlauf nach, unabhängig vom
Kilometerstand-Status. fakeTrack() (eine erfundene Linie) ist entfernt, die
Karte zeichnet jetzt die echte Route oder eine ehrliche Gerade zwischen den
bekannten Punkten. CARTOs anonyme Kartenkacheln verlangen inzwischen einen
API-Schlüssel ("API key required" auf jeder Karte in beiden Apps) - auf
schlüssellose OpenStreetMap-Kacheln umgestellt. Start/Ziel zeigen jetzt Datum
und Uhrzeit über der Adresse; die fehlerhafte "Status"-Zeile ist entfernt.
Batteriespannungs-Diagramm nutzt auf großen Bildschirmen die volle
Spaltenbreite statt einer festen 400px-Deckelung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,19 @@ Ladezustands-/Gesundheitsauswertung im Frontend (AGM_RUHE_MAX_V) den
|
||||
Tagesminimalwert, nicht den Maximalwert.
|
||||
|
||||
Es wird bewusst NICHT gefiltert, welche Messwerte gespeichert werden - min UND
|
||||
max jedes Tages werden roh aufgezeichnet. Die AGM-Ruhespannungs-Grenze wird
|
||||
max jedes Tages werden roh aufgezeichnet, solange die Messung überhaupt
|
||||
plausibel ist (siehe SPANNUNG_MIN_V unten). Die AGM-Ruhespannungs-Grenze wird
|
||||
erst beim Anzeigen angewendet: Tage, an denen selbst der Minimalwert über der
|
||||
Grenze liegt (Fahrzeug nie im Ruhezustand beobachtet, z. B. bei einer sehr
|
||||
langen Fahrt), tauchen dort nicht in der Statistik auf.
|
||||
|
||||
Eine Ausnahme von "roh aufzeichnen" gibt es: Werte unter SPANNUNG_MIN_V sind
|
||||
für eine 12V-Bleibatterie physikalisch nicht plausibel (selbst tiefentladen
|
||||
liegt die Ruhespannung deutlich darüber) und deuten auf einen Sensor-/
|
||||
Verbindungsfehler statt auf eine echte Messung hin - sie werden gar nicht erst
|
||||
aufgezeichnet, damit ein einzelner Ausreißer nicht als Tagesminimum in der
|
||||
Liste und im Diagramm landet.
|
||||
|
||||
Ohne zugeordneten BATTERIE_SENSOR passiert hier bewusst nichts, statt
|
||||
Platzhalter-Nullwerte aufzuzeichnen.
|
||||
|
||||
@@ -48,6 +56,10 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Physikalisch plausible Untergrenze für eine 12V-Bleibatterie - darunter ist
|
||||
# es ein Sensor-/Verbindungsfehler, keine echte Messung (siehe Moduldocstring).
|
||||
SPANNUNG_MIN_V = 8.0
|
||||
|
||||
|
||||
async def pruefen(k: Koordinator) -> None:
|
||||
sensor = k.zuordnung.werte.BATTERIE_SENSOR
|
||||
@@ -60,6 +72,12 @@ async def pruefen(k: Koordinator) -> None:
|
||||
spannung = float(wert)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if spannung < SPANNUNG_MIN_V:
|
||||
_LOGGER.warning(
|
||||
"Batteriespannung %s V unter der Plausibilitätsgrenze (%s V) - nicht erfasst",
|
||||
spannung, SPANNUNG_MIN_V,
|
||||
)
|
||||
return
|
||||
|
||||
aussentemp = None
|
||||
temp_wert = zustand_oder_none(k.hass, k.zuordnung.werte.AUSSENTEMP_SENSOR)
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.8.25.8","sha256":"464c294c6c07dcb636a8ea3bd93c7e525b21722defb41f573e2b31fefade49ea","bytes":235525,"gebaut":"2026-08-27T09:40:45Z"}
|
||||
{"version":"2026.8.27.1","sha256":"d7cd168032a93ed8430e3a86ee583c635004fc3518eecf0d1e69abd78741ebc4","bytes":235552,"gebaut":"2026-08-27T15:13:25Z"}
|
||||
Binary file not shown.
@@ -377,6 +377,10 @@ const dedat = (d) => (gueltigesDatum(d)
|
||||
? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
|
||||
: "–");
|
||||
function dat(s) { const [d, m, j] = s.split(".").map(Number); return new Date(j, m - 1, d); }
|
||||
/* Datum + Uhrzeit in einer Zeile, für Start-/Zielzeitpunkt der Einzelfahrt. */
|
||||
const dezeitpunkt = (d) => (gueltigesDatum(d)
|
||||
? `${dedat(d)} · ${d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr`
|
||||
: "–");
|
||||
|
||||
const MISCHDAUER = 180;
|
||||
function tagesleistung() {
|
||||
@@ -449,9 +453,14 @@ let BELEG_STATION_POS = null;
|
||||
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
|
||||
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
|
||||
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
|
||||
dark_all-Kacheln liessen die Karte im Panel schwarz wirken. */
|
||||
const TILE_URL = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
|
||||
const ATTR = "© OpenStreetMap · CARTO";
|
||||
dark_all-Kacheln liessen die Karte im Panel schwarz wirken.
|
||||
CARTOs anonyme basemaps.cartocdn.com-Kacheln (vorher hier) verlangen
|
||||
inzwischen einen eigenen API-Schlüssel - ohne ihn zeigten die Kacheln nur
|
||||
noch "API key required". Die Standard-OpenStreetMap-Kacheln sind weiterhin
|
||||
ohne Schlüssel nutzbar und decken diesen Bedarf (ein Fahrzeug, geringes
|
||||
Kartenaufkommen) ohne eigenen Dienst. */
|
||||
const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const ATTR = "© OpenStreetMap contributors";
|
||||
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
|
||||
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
|
||||
const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
|
||||
@@ -527,15 +536,6 @@ function kartenMittelpunkt() {
|
||||
return pos && pos.length === 2 ? pos : [51.1657, 10.4515];
|
||||
}
|
||||
|
||||
function fakeTrack(a, b, n = 13) {
|
||||
const pts = []; let seed = Math.round((a[0] + b[1]) * 1000);
|
||||
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff - 0.5; };
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n, k = Math.sin(t * Math.PI);
|
||||
pts.push([a[0] + (b[0] - a[0]) * t + rnd() * 0.05 * k, a[1] + (b[1] - a[1]) * t + rnd() * 0.05 * k]);
|
||||
}
|
||||
pts[0] = a; pts[n] = b; return pts;
|
||||
}
|
||||
function tiles() {
|
||||
if (!MAP || !window.L) return;
|
||||
const url = TILE_URL;
|
||||
@@ -552,14 +552,20 @@ async function initMap() {
|
||||
if (route.name === "trip") {
|
||||
const t = TRIPS.find((x) => x.trip_id === route.id);
|
||||
if (t && t.start_lat != null && t.end_lat != null) {
|
||||
const track = fakeTrack([t.start_lat, t.start_lon], [t.end_lat, t.end_lon]);
|
||||
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9 }).addTo(MAP);
|
||||
// Echte Streckenlinie aus dem GPS-Verlauf (route, screening.py), wenn
|
||||
// vorhanden - sonst eine ehrliche gerade Linie zwischen Start und Ziel,
|
||||
// keine erfundene Kurve mehr (siehe companion-app/Karte.tsx, dieselbe
|
||||
// Haltung: keine Route zu zeigen, die es so nicht gab).
|
||||
const track = Array.isArray(t.route) && t.route.length >= 2
|
||||
? t.route
|
||||
: [[t.start_lat, t.start_lon], [t.end_lat, t.end_lon]];
|
||||
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9, dashArray: Array.isArray(t.route) && t.route.length >= 2 ? null : "4 6" }).addTo(MAP);
|
||||
track.slice(1, -1).forEach((pt) => window.L.circleMarker(pt, { radius: 2.5, color: rot, weight: 0, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP));
|
||||
window.L.circleMarker([t.start_lat, t.start_lon], { radius: 6, color: "#fff", weight: 2, fillColor: "#000", fillOpacity: 1 }).addTo(MAP).bindPopup("Start · " + (t.start_address || ""));
|
||||
window.L.circleMarker([t.end_lat, t.end_lon], { radius: 6, color: rot, weight: 2, fillColor: rot, fillOpacity: 1 }).addTo(MAP).bindPopup("Ziel · " + (t.end_address || ""));
|
||||
MAP.fitBounds(line.getBounds(), { padding: [26, 26] });
|
||||
} else {
|
||||
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten (§7a-Backend-Lücke)
|
||||
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten
|
||||
}
|
||||
} else if (route.name === "fill") {
|
||||
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
|
||||
@@ -1804,7 +1810,7 @@ function vBatterieverlauf() {
|
||||
</div>
|
||||
${genug ? `
|
||||
<div id="bvChart" style="margin-top:14px;touch-action:none;position:relative">
|
||||
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;height:170px;display:block"></svg>
|
||||
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;aspect-ratio:${BV_W}/${BV_H};display:block"></svg>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
|
||||
<span class="label" id="bvRange" style="margin-top:0"></span>
|
||||
@@ -2886,8 +2892,8 @@ function vReifen() {
|
||||
<button type="button" data-bildersetzen="${radDatei}">Foto ersetzen</button>
|
||||
<button type="button" class="loeschen" data-bildloeschen="${radDatei}">Löschen</button>
|
||||
</div>` : ""}
|
||||
<button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="montiert ${aktiv ? "on" : ""}">Montiert</button>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="montiert ${aktiv ? "on" : ""}">Montiert</button>
|
||||
<button class="iconbtn" data-reifenarchivieren="${k}" aria-label="Neue Räder anlegen">${ciSVG(CI.packageS, 18)}</button>
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
@@ -3150,13 +3156,12 @@ function vTrip(id) {
|
||||
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
|
||||
</div>
|
||||
<div class="tile"><dl class="rows">
|
||||
<div class="row"><dt>Startort</dt><dd>${esc(t.start_address || "unbekannt")}<small>${dedat(new Date(t.ts_start))}</small></dd></div>
|
||||
<div class="row"><dt>Zielort</dt><dd>${esc(t.end_address || "unbekannt")}</dd></div>
|
||||
<div class="row"><dt>Start</dt><dd>${dezeitpunkt(new Date(t.ts_start))}<small>${esc(t.start_address || "unbekannt")}</small></dd></div>
|
||||
<div class="row"><dt>Ziel</dt><dd>${dezeitpunkt(new Date(t.ts_end))}<small>${esc(t.end_address || "unbekannt")}</small></dd></div>
|
||||
<div class="row"><dt>Startkilometer</dt><dd>${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}</dd></div>
|
||||
<div class="row"><dt>Endkilometer</dt><dd>${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}</dd></div>
|
||||
<div class="row"><dt>Dauer</dt><dd>${dauerText(t.duration_s)}</dd></div>
|
||||
<div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
|
||||
<div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || "–"}</small></dd></div>
|
||||
</dl>
|
||||
<button class="aktion" data-fahrtbearbeiten>Werte bearbeiten</button>
|
||||
</div>
|
||||
|
||||
@@ -336,12 +336,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
.standortmenu{max-width:640px;margin-left:auto;margin-right:auto}
|
||||
|
||||
/* Batteriespannungs-Diagramm ist auf BV_W=320 SVG-Einheiten gezeichnet
|
||||
(audi-dashboard-app.js) und haelt seine Hoehe fest bei 170px - bei voller
|
||||
Spaltenbreite (772px Inhaltsbreite: 860 - 2*44px Seitenrand) zog
|
||||
preserveAspectRatio="none" das Diagramm zu einem extrem breiten, flachen
|
||||
Band auseinander. Gedeckelt wie die Popups oben, statt die feste Hoehe
|
||||
oder die BV_*-Zeichen-Logik anzufassen. */
|
||||
#bvChart{max-width:400px;margin-left:auto;margin-right:auto}
|
||||
(audi-dashboard-app.js), vorher mit fester Hoehe von 170px bei
|
||||
preserveAspectRatio="none" - bei voller Spaltenbreite (772px
|
||||
Inhaltsbreite: 860 - 2*44px Seitenrand) zog das Diagramm zu einem extrem
|
||||
breiten, flachen Band auseinander, gedeckelt mit max-width:400px (macht
|
||||
es dafuer auf grossen Bildschirmen unnoetig klein). Das SVG traegt jetzt
|
||||
ein aspect-ratio passend zum viewBox-Verhaeltnis (320/170), wodurch die
|
||||
Hoehe mit der Breite mitwaechst - das Diagramm darf deshalb die volle
|
||||
Spaltenbreite nutzen, ohne wieder zu verzerren. */
|
||||
}
|
||||
|
||||
/* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt
|
||||
|
||||
@@ -41,9 +41,10 @@ import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .batterie import SPANNUNG_MIN_V
|
||||
from .fahrterkennung import leere_fahrt, pausenzeit_sekunden
|
||||
from .tankerkennung import LITER_SCHWELLE, leerer_tankvorgang, schwelle_prozent
|
||||
from .verlauf import Verlaufspunkt, verlauf_lesen, wert_bei, zahl
|
||||
from .verlauf import Verlaufspunkt, route_aus_verlauf, verlauf_lesen, wert_bei, zahl
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .koordinator import Koordinator
|
||||
@@ -193,6 +194,7 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
"start_lon": wert_bei(lon_verlauf, f_start),
|
||||
"end_lat": wert_bei(lat_verlauf, f_ende),
|
||||
"end_lon": wert_bei(lon_verlauf, f_ende),
|
||||
"route": route_aus_verlauf(lat_verlauf, lon_verlauf, f_start, f_ende),
|
||||
"status": "vollständig" if distanz is not None else "offen",
|
||||
})
|
||||
neue.append(fahrt)
|
||||
@@ -305,7 +307,12 @@ async def _batterie_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
tage: dict[str, dict] = {}
|
||||
for ts, wert in verlauf:
|
||||
spannung = zahl(wert)
|
||||
if spannung is None:
|
||||
# Unter SPANNUNG_MIN_V ist es kein echter Messwert, sondern ein
|
||||
# Sensor-/Verbindungsfehler (siehe batterie.py) - live wie
|
||||
# rückwirkend gleich behandelt, sonst landete ein Ausreißer aus dem
|
||||
# Verlauf als Tagesminimum, wo die Live-Erfassung ihn nie hätte
|
||||
# aufzeichnen können.
|
||||
if spannung is None or spannung < SPANNUNG_MIN_V:
|
||||
continue
|
||||
tag = ts.date().isoformat()
|
||||
eintrag = tage.get(tag)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.25.8",
|
||||
"version": "2026.8.27.1",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": ["@paul"],
|
||||
|
||||
@@ -22,7 +22,7 @@ import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .verlauf import naechster_wert, verlauf_lesen
|
||||
from .verlauf import naechster_wert, route_aus_verlauf, verlauf_lesen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .koordinator import Koordinator
|
||||
@@ -56,18 +56,31 @@ def _vollstaendig(aenderungen: dict, fahrt: dict) -> dict:
|
||||
|
||||
|
||||
async def durchfuehren(k: Koordinator) -> None:
|
||||
"""Versucht, alle offenen Fahrten zu vervollständigen."""
|
||||
"""Versucht, alle offenen Fahrten zu vervollständigen, und trägt allen
|
||||
Fahrten ohne Standortdaten Start-/Zielposition sowie eine Streckenlinie
|
||||
nach - unabhängig vom Kilometerstand-Status, da beide Datenquellen
|
||||
unabhängig voneinander verspätet eintreffen können."""
|
||||
km_sensor = k.zuordnung.werte.KM_SENSOR
|
||||
fahrten = await k.ablage.fahrten_lesen()
|
||||
offene = [f for f in fahrten if f.get("status") == "offen"]
|
||||
if not offene:
|
||||
if not fahrten:
|
||||
return
|
||||
offene.sort(key=lambda f: f.get("ts_start", ""))
|
||||
|
||||
offene = [f for f in fahrten if f.get("status") == "offen"]
|
||||
if km_sensor:
|
||||
offene.sort(key=lambda f: f.get("ts_start", ""))
|
||||
for fahrt in offene:
|
||||
await _fahrt_screenen(k, km_sensor, fahrt)
|
||||
|
||||
lat_sensor = k.zuordnung.werte.STANDORT_LAT_SENSOR
|
||||
lon_sensor = k.zuordnung.werte.STANDORT_LON_SENSOR
|
||||
if lat_sensor and lon_sensor:
|
||||
ohne_position = [
|
||||
f for f in fahrten
|
||||
if f.get("start_lat") is None or f.get("end_lat") is None or f.get("route") is None
|
||||
]
|
||||
for fahrt in ohne_position:
|
||||
await _position_screenen(k, lat_sensor, lon_sensor, fahrt)
|
||||
|
||||
await _lueckenlos_verketten(k)
|
||||
await k.fahrten_veroeffentlichen()
|
||||
|
||||
@@ -105,6 +118,49 @@ async def _fahrt_screenen(k: Koordinator, km_sensor: str, fahrt: dict) -> None:
|
||||
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], _vollstaendig(aenderungen, fahrt))
|
||||
|
||||
|
||||
async def _position_screenen(
|
||||
k: Koordinator, lat_sensor: str, lon_sensor: str, fahrt: dict
|
||||
) -> None:
|
||||
"""Trägt Start-/Zielposition und eine echte Streckenlinie nach, sobald der
|
||||
GPS-Verlauf sie hergibt - unabhängig vom Kilometerstand-Status.
|
||||
|
||||
Dieselbe "Fahrt gehört der Vergangenheit an" wie bei den Kilometerständen:
|
||||
kein Live-Zustand zum Fahrtbeginn/-ende wird herangezogen (der wäre bei
|
||||
einer Funklücke des Trackers unzuverlässig, siehe fahrterkennung.py), nur
|
||||
der aufgezeichnete Verlauf im Fahrtfenster."""
|
||||
start = _als_zeit(fahrt.get("ts_start"))
|
||||
ende = _als_zeit(fahrt.get("ts_end"))
|
||||
if start is None or ende is None:
|
||||
return
|
||||
|
||||
lat_verlauf = await verlauf_lesen(k.hass, lat_sensor, start - SUCHFENSTER, ende + SUCHFENSTER)
|
||||
lon_verlauf = await verlauf_lesen(k.hass, lon_sensor, start - SUCHFENSTER, ende + SUCHFENSTER)
|
||||
if not lat_verlauf or not lon_verlauf:
|
||||
return
|
||||
|
||||
aenderungen: dict = {}
|
||||
if fahrt.get("start_lat") is None:
|
||||
lat = naechster_wert(start, lat_verlauf)
|
||||
lon = naechster_wert(start, lon_verlauf)
|
||||
if lat is not None and lon is not None:
|
||||
aenderungen["start_lat"] = lat
|
||||
aenderungen["start_lon"] = lon
|
||||
if fahrt.get("end_lat") is None:
|
||||
lat = naechster_wert(ende, lat_verlauf)
|
||||
lon = naechster_wert(ende, lon_verlauf)
|
||||
if lat is not None and lon is not None:
|
||||
aenderungen["end_lat"] = lat
|
||||
aenderungen["end_lon"] = lon
|
||||
if fahrt.get("route") is None:
|
||||
route = route_aus_verlauf(lat_verlauf, lon_verlauf, start, ende)
|
||||
if route is not None:
|
||||
aenderungen["route"] = route
|
||||
|
||||
if aenderungen:
|
||||
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
|
||||
fahrt.update(aenderungen)
|
||||
|
||||
|
||||
async def _lueckenlos_verketten(k: Koordinator) -> None:
|
||||
"""Fahrt-Ende(N) == Fahrt-Start(N+1), solange dazwischen nicht gefahren
|
||||
wurde.
|
||||
|
||||
@@ -106,6 +106,34 @@ def wert_bei(
|
||||
return davor
|
||||
|
||||
|
||||
def route_aus_verlauf(
|
||||
lat_verlauf: list[Verlaufspunkt],
|
||||
lon_verlauf: list[Verlaufspunkt],
|
||||
start: datetime.datetime,
|
||||
ende: datetime.datetime,
|
||||
) -> list[list[float]] | None:
|
||||
"""Baut eine chronologische Liste von [lat, lon]-Punkten für die
|
||||
Fahrt-Detailkarte, aus zwei getrennt geführten Sensor-Verläufen (die
|
||||
FMM003 liefert Breiten-/Längengrad als zwei unabhängige Sensoren, siehe
|
||||
STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR). Jeder Breitengrad-Punkt im
|
||||
Fenster [start, ende] bekommt den zeitlich nächstgelegenen Längengrad-Wert
|
||||
zugeordnet - beide Sensoren aktualisieren unabhängig voneinander, exakt
|
||||
gleiche Zeitstempel sind nicht garantiert. None bei weniger als zwei
|
||||
Punkten (keine sinnvolle Linie)."""
|
||||
punkte: list[list[float]] = []
|
||||
for ts, wert in lat_verlauf:
|
||||
if not (start <= ts <= ende):
|
||||
continue
|
||||
lat = zahl(wert)
|
||||
if lat is None:
|
||||
continue
|
||||
lon = naechster_wert(ts, lon_verlauf)
|
||||
if lon is None:
|
||||
continue
|
||||
punkte.append([lat, lon])
|
||||
return punkte if len(punkte) >= 2 else None
|
||||
|
||||
|
||||
def naechster_wert(
|
||||
zielzeit: datetime.datetime, verlauf: list[Verlaufspunkt]
|
||||
) -> float | None:
|
||||
|
||||
Reference in New Issue
Block a user