services: services

obd_diag.services.diagnostics

.. py:module:: obd_diag.services.diagnostics

Diagnose-Abläufe: Fehlercodes lesen und mit Klartext anreichern.

.. py:function:: check_low_voltage(elm, voltage) :module: obd_diag.services.diagnostics

Bestätigt eine niedrige Adapter-Messung (ATRV) mit der Steuergerätespannung.

Der Adapter misst an Pin 16 der Diagnosebuchse, oft hinter einer Schutzdiode: Am Mercedes A 180 d (W177) zeigte er 11,2 V, das Motorsteuergerät meldete 12,0 V. Liegt voltage unter LOW_VOLTAGE, wird daher PID 42 gelesen und, wenn plausibel, statt voltage geliefert (auch wenn er niedriger ist). Sonst, auch bei None, bleibt es bei voltage. Nur nach ausgehandeltem Protokoll aufrufen (sonst liefe die Protokollsuche mit der kurzen Wartezeit).

.. py:class:: DtcKind(*values) :module: obd_diag.services.diagnostics

Bases: :py:class:~enum.StrEnum

.. py:class:: DiagnosticCode(code: str, kind: obd_diag.services.diagnostics.DtcKind, info: obd_diag.data.dtc_catalog.DtcInfo | None = None, online: obd_diag.services.dtc_online.OnlineExplanation | None = None) :module: obd_diag.services.diagnostics

Bases: :py:class:object

.. py:class:: ScanResult(adapter: str, protocol: str, voltage: float | None, codes: list[obd_diag.services.diagnostics.DiagnosticCode] = ) :module: obd_diag.services.diagnostics

Bases: :py:class:object

.. py:class:: DtcLookup(*args, **kwargs) :module: obd_diag.services.diagnostics

Bases: :py:class:~typing.Protocol

Was scan vom Katalog braucht; erfüllt z. B. von DtcCatalog.

.. py:function:: scan(elm, catalog, lang=None) :module: obd_diag.services.diagnostics

Initialisiert den Adapter und liest gespeicherte, ausstehende und permanente Codes.

Nur lesend: es wird nichts gelöscht (kein Mode 04). lang: Sprache der Katalogtexte, ohne Angabe die eingestellte (obd_diag.i18n).

.. py:function:: add_online_explanations(result, manufacturer, lookup=None) :module: obd_diag.services.diagnostics

Ergänzt Codes ohne Katalogtext um eine Online-Erklärung (nur nach Opt-in).

Codes mit Katalogtext werden nicht nachgeschlagen; ohne solche Codes geht nichts ins Netz. manufacturer kommt aus der FIN, sonst None.

.. py:function:: scan_to_dict(result) :module: obd_diag.services.diagnostics

JSON-taugliche Form eines Scans (wie obd-diag scan --json).

obd_diag.services.clear

.. py:module:: obd_diag.services.clear

Fehlercodes löschen (Mode 04): die einzige schreibende Aktion des Tools.

Ablauf: Vorbedingungen prüfen (Zündung an, Motor aus), Codes und Freeze Frame sichern, erst dann löschen und zur Kontrolle neu einlesen. Schlägt irgendein Schritt vor dem Löschen fehl, wird Mode 04 nicht gesendet.

Derzeit gesperrt (CLEAR_ENABLED): Erst wenn das Lesen an einem echten Fahrzeug geprüft ist, wird Löschen freigegeben. Die Sperre greift in clear_codes vor der ersten Anfrage; CLI und GUI fragen clear_enabled zusätzlich vorher ab.

.. py:function:: clear_enabled() :module: obd_diag.services.clear

Ob Löschen freigegeben ist (zur Laufzeit gelesen, damit Tests es umstellen können).

.. py:exception:: ClearRefused :module: obd_diag.services.clear

Bases: :py:class:Exception

Eine Vorbedingung ist nicht erfüllt; die Meldung ist für Nutzer gedacht.

.. py:class:: ClearResult(backup_path: pathlib.Path, before: obd_diag.services.diagnostics.ScanResult, after: obd_diag.services.diagnostics.ScanResult) :module: obd_diag.services.clear

Bases: :py:class:object

.. py:function:: clearable_codes(result) :module: obd_diag.services.clear

Die Codes aus result, die Mode 04 löschen würde.

.. py:function:: default_backup_dir() :module: obd_diag.services.clear

$XDG_DATA_HOME/obd-diag/backups (Standard: ~/.local/share/obd-diag/backups).

.. py:function:: check_preconditions(elm) :module: obd_diag.services.clear

Wirft ClearRefused, wenn nicht gelöscht werden darf (z. B. Motor läuft).

Geprüft wird: das Steuergerät antwortet (Zündung an), die Bordspannung ist nicht zu niedrig und die Drehzahl ist 0 (Motor aus), bei mehreren antwortenden Steuergeräten bei jedem. Ist eine Drehzahl nicht lesbar, wird vorsichtshalber ebenfalls abgelehnt. Sendet nur lesende Befehle.

.. py:function:: clear_codes(elm, catalog, *, backup_dir=None, lang=None) :module: obd_diag.services.clear

Löscht die Fehlercodes nach Prüfung und Sicherung und liest danach neu ein.

Reihenfolge: Scan, Vorbedingungen, Freeze Frame lesen, Sicherung schreiben, erst dann Mode 04, zum Schluss Kontroll-Scan. ClearRefused vor dem Löschen heißt: nichts wurde verändert. Lehnt das Steuergerät Mode 04 ab, bleibt die Sicherung liegen und ClearRefused nennt den Grund. Ist Löschen gesperrt (CLEAR_ENABLED), kommt ClearRefused sofort, ohne dass etwas gesendet wird. lang: Sprache der Katalogtexte, ohne Angabe die eingestellte (obd_diag.i18n).

obd_diag.services.readiness

.. py:module:: obd_diag.services.readiness

Readiness-Monitore (Mode 01, PID 01): Status der Eigendiagnosen fürs Abgas-System.

Aufbau der vier Datenbytes A-D nach SAE J1979 (ISO 15031-5), Tabelle zu Mode 01 PID 01 wie in Wikipedia „OBD-II PIDs“ (Abschnitt „Service 01 PID 01“, abgerufen 2026-10)::

   A7      MIL (Kontrollleuchte) an
   A6-A0   Zahl der gespeicherten Fehlercodes (dieses Steuergeräts)
   B3      0 = Ottomotor (spark ignition), 1 = Diesel (compression ignition)
   B0/B4   Verbrennungsaussetzer   unterstützt / nicht abgeschlossen
   B1/B5   Kraftstoffsystem        unterstützt / nicht abgeschlossen
   B2/B6   Komponenten             unterstützt / nicht abgeschlossen
   B7      reserviert
   C       Bit n: Monitor n unterstützt
   D       Bit n: Monitor n nicht abgeschlossen

   Bit  Otto (B3 = 0)              Diesel (B3 = 1)
   0    Katalysator                NMHC-Katalysator
   1    Katalysatorheizung         NOx-Nachbehandlung / SCR
   2    Tankentlüftung (EVAP)      reserviert
   3    Sekundärluftsystem         Ladedruck
   4    Klimaanlagen-Kältemittel   reserviert
   5    Lambdasonde                Abgassensor
   6    Lambdasondenheizung        Partikelfilter
   7    AGR und/oder VVT           AGR und/oder VVT

Bit C4/D4 (Otto: Kältemittel der Klimaanlage) ist in neueren J1979-Ausgaben reserviert, wird aber weiter dekodiert, weil ältere Fahrzeuge es setzen. Reservierte Bits werden ignoriert. Nicht unterstützte Monitore erscheinen mit NOT_SUPPORTED.

Bezug zur Abgasuntersuchung (AU): Das Ergebnis heißt bewusst „alle Tests abgeschlossen“ und nicht „AU-bereit“. Nach der AU-Richtlinie (Verkehrsblatt 19/2017, Leitfaden 5.01, gültig ab 01.01.2018) besteht die AU bei allen OBD-Fahrzeugen aus einer Funktionsprüfung OBD und einer Endrohrmessung; die Bewertung der Prüfbereitschaftstests entfällt, wenn alle unterstützten Tests durchgeführt sind (Hella Gutmann, „Informationen zum Leitfaden 5.01“, BD0059, 12/2017). Schon vorher galt: Ist der Readiness-Code nicht gesetzt, wird stattdessen gemessen; das allein ist kein erheblicher Mangel. Eine zitierfähige, allgemeine Liste tolerierter offener Monitore gibt es nicht (Stand 2026-10-07), die Behandlung hängt von Fahrzeug und Prüfablauf ab. Deshalb gibt es hier keine AU-Bewertung.

.. py:class:: MonitorState(*values) :module: obd_diag.services.readiness

Bases: :py:class:~enum.StrEnum

.. py:class:: Monitor(key: str, name: str, state: obd_diag.services.readiness.MonitorState) :module: obd_diag.services.readiness

Bases: :py:class:object

.. py:class:: ReadinessStatus(mil_on: bool, dtc_count: int, compression_ignition: bool, monitors: tuple[obd_diag.services.readiness.Monitor, …]) :module: obd_diag.services.readiness

Bases: :py:class:object

.. py:property:: ReadinessStatus.all_complete :module: obd_diag.services.readiness :type: bool

  Alle unterstützten Monitore abgeschlossen.

  Das ist kein AU-Ergebnis: Seit der AU-Richtlinie 2018 (Leitfaden 5.01) gehört zur
  AU immer auch die Endrohrmessung; offene Monitore führen nicht pauschal zum
  Nichtbestehen, und ob einzelne toleriert werden, hängt von Fahrzeug und
  Prüfablauf ab (siehe Doku, docs/benutzen/readiness.md).

.. py:property:: ReadinessStatus.ready :module: obd_diag.services.readiness :type: bool

  Alter Name für ``all_complete`` (bleibt zur Kompatibilität).

.. py:function:: specific_monitors(compression_ignition) :module: obd_diag.services.readiness

Motorspezifische Monitore in Byte C/D als (Bit, Schlüssel, Name).

Gleicher Aufbau in PID 01 und PID 41 (services/readiness_assist.py).

.. py:function:: decode_readiness(data) :module: obd_diag.services.readiness

Dekodiert die vier Datenbytes A-D der Antwort auf 0101.

Weitere Bytes werden ignoriert; ValueError bei weniger als vier.

.. py:function:: combine_readiness(statuses) :module: obd_diag.services.readiness

Fasst die Antworten mehrerer Steuergeräte zu einem Gesamtstand zusammen.

Das Ergebnis hängt nicht von der Reihenfolge der Antworten ab. Die Motorart kommt aus _ignition_type; Steuergeräte mit anderer Motorart-Kennung zählen nur für die allgemeinen Monitore aus Byte B (Aussetzer, Kraftstoffsystem, Komponenten), weil ihre Bits C/D anders belegt sind. MIL: an, wenn ein Steuergerät sie meldet; Codezahl: Summe (Mode 03 liefert ja auch die Codes aller Steuergeräte). Je Monitor zählt der schlechteste Stand: nicht abgeschlossen vor abgeschlossen vor nicht unterstützt.

.. py:function:: read_readiness(elm) :module: obd_diag.services.readiness

None, wenn das Steuergerät PID 01 nicht beantwortet.

Antworten mehrere Steuergeräte (z. B. Motor und Getriebe), werden sie mit combine_readiness zusammengefasst. Negative oder zu kurze Antworten zählen als nicht beantwortet. ElmError bei Antworten, die keine Hex-Daten sind.

obd_diag.services.readiness_assist

.. py:module:: obd_diag.services.readiness_assist

Readiness-Assistent: welche Tests noch offen sind und welche Fahrt sie abschließt.

Grundlage sind zwei Mode-01-PIDs nach SAE J1979 (Wikipedia „OBD-II PIDs“, Service 01, PID 01 und PID 41, abgerufen 2026-10):

  • PID 01 meldet den Stand seit dem Löschen der Fehlercodes (services/readiness.py).

  • PID 41 meldet den Stand in diesem Fahrzyklus, also seit dem Einschalten der Zündung bzw. dem Motorstart. Byte A ist reserviert, die Bytes B bis D sind aufgebaut wie bei PID 01, nur heißt das erste Bit je Monitor nicht „unterstützt“, sondern „in diesem Zyklus freigegeben“ (0 = ausgesetzt oder nicht vorhanden), das zweite „in diesem Zyklus nicht abgeschlossen“. Ein nicht freigegebener Monitor meldet dort immer 0.

Welche Belegung Byte C/D hat (Otto oder Diesel), kommt aus der zusammengefassten Antwort auf PID 01 (combine_readiness), nicht aus Byte B von PID 41: dessen Bit 3 ist nicht in allen Ausgaben der Norm als Motorart belegt. Antworten mehrere Steuergeräte, zählt je Monitor der schlechteste Stand (noch offen vor abgeschlossen vor ausgesetzt); das hängt nicht von der Reihenfolge der Antworten ab (ohne Header wechselt sie am W177 bei jeder Anfrage).

Die Fahrhinweise sind allgemeine Erfahrungswerte, keine Norm: J1979 legt nur fest, wie der Status gemeldet wird. Wann ein Monitor läuft (Temperaturen, Geschwindigkeit, Dauer, Tankfüllung), bestimmt jeder Hersteller selbst, und oft braucht es mehrere Fahrten. Gesendet werden nur lesende Anfragen: initialize, protocol, 0101, 0141.

.. py:class:: CycleState(*values) :module: obd_diag.services.readiness_assist

Bases: :py:class:~enum.StrEnum

.. py:class:: CycleMonitor(key: str, name: str, state: obd_diag.services.readiness_assist.CycleState) :module: obd_diag.services.readiness_assist

Bases: :py:class:object

.. py:class:: DriveCycleStatus(monitors) :module: obd_diag.services.readiness_assist

Bases: :py:class:object

Antwort auf PID 41: Stand der Monitore in diesem Fahrzyklus.

.. py:function:: decode_drive_cycle(data, compression_ignition) :module: obd_diag.services.readiness_assist

Dekodiert die vier Datenbytes A-D der Antwort auf 0141.

compression_ignition (aus PID 01) wählt die Belegung von Byte C/D. Weitere Bytes werden ignoriert; ValueError bei weniger als vier.

.. py:function:: combine_drive_cycle(statuses) :module: obd_diag.services.readiness_assist

Je Monitor der schlechteste Stand aller Steuergeräte (unabhängig von der Reihenfolge).

Alle Antworten müssen mit derselben Motorart dekodiert sein.

.. py:function:: read_drive_cycle(elm, compression_ignition) :module: obd_diag.services.readiness_assist

0141; None, wenn kein Steuergerät PID 41 beantwortet.

Negative oder zu kurze Antworten zählen als nicht beantwortet, mehrere Steuergeräte werden mit combine_drive_cycle zusammengefasst. ElmError bei Antworten, die keine Hex-Daten sind.

.. py:class:: Advice(key, name, cycle, hint) :module: obd_diag.services.readiness_assist

Bases: :py:class:object

Ein seit dem Löschen offener Test mit Stand dieser Fahrt und Fahrhinweis.

.. py:function:: advise(readiness, cycle) :module: obd_diag.services.readiness_assist

Die seit dem Löschen offenen Tests in der Reihenfolge von PID 01.

.. py:function:: notes(readiness, cycle, *, cycle_read=True) :module: obd_diag.services.readiness_assist

Allgemeine Hinweise (deutsch, zur Anzeige tr); leer, wenn nichts offen ist.

cycle_read: PID 41 wurde abgefragt (cycle ist None also, weil das Fahrzeug nicht antwortet, nicht weil niemand gefragt hat).

.. py:class:: ReadinessCheck(created, adapter, protocol, readiness, drive_cycle) :module: obd_diag.services.readiness_assist

Bases: :py:class:object

Ergebnis von check_readiness.

.. py:function:: check_readiness(elm) :module: obd_diag.services.readiness_assist

Adapter initialisieren, Protokoll aushandeln, PID 01 und PID 41 lesen; nur lesend.

Befehle: initialize (ATZ … ATSP0), protocol (0100, ATDPN, ATDP), 0101 und, wenn PID 01 beantwortet wurde, 0141. Ein ElmError bei PID 01 oder 41 lässt nur diesen Teil leer; Verbindungsfehler (TransportError) gehen an den Aufrufer.

.. py:function:: check_to_dict(check) :module: obd_diag.services.readiness_assist

JSON-taugliche Form (obd-diag readiness --json); Texte in der eingestellten Sprache, Schlüssel und Zustände wie im Code.

obd_diag.services.vehicle

.. py:module:: obd_diag.services.vehicle

Fahrzeug-Identifikation: FIN lesen (Mode 09, PID 02) und dekodieren.

Offline liefert eine WMI-Tabelle (FIN-Stellen 1-3) Hersteller und Land. Die NHTSA-vPIC-Abfrage ist optional, nur nach Opt-in, und wird je FIN lokal gecacht.

Regeln der Dekodierung (decode_vin):

  • Gültig: genau 17 Zeichen aus 0-9 und A-Z ohne I, O, Q (ISO 3779).

  • Prüfziffer (Stelle 9, Gewichte nach 49 CFR 565.15 / ISO 3779 Anhang): vorgeschrieben nur für Nordamerika (WMI beginnt mit 1-5) und China (L, GB 16735); dort True/False. Sonst (z. B. Europa, oft Z an Stelle 9) True, wenn sie zufällig oder freiwillig stimmt, und None, wenn nicht: ein Abweichen ist dort kein Fehler.

  • Modelljahr (Stelle 10): der Code wiederholt sich alle 30 Jahre (A = 1980 oder 2010). In Nordamerika entscheidet Stelle 7 (49 CFR 565.15: Ziffer = 1980-2009, Buchstabe = 2010-2039); das gilt dort für Pkw, MPV und Lkw bis 10.000 lb, nicht für schwerere Fahrzeuge, Busse und Motorräder, wird hier aber für alle FIN mit 1-5 angewandt. Sonst gilt als beste Schätzung (model_year) das jüngste Jahr, das nicht mehr als ein Jahr in der Zukunft liegt (Modelljahre beginnen vor dem Kalenderjahr); das 30 Jahre ältere Jahr steht dann in model_year_alternatives, sofern es nicht vor 1980 liegt (Beginn der Codes) und zum Protokoll passt (unten). Außerhalb Nordamerikas ist Stelle 10 nicht überall als Modelljahr genutzt, die Angabe ist dort ein Hinweis, keine Gewissheit. Bekannt anders belegt ist sie bei Mercedes-Benz (MODEL_YEAR_NOT_ENCODED): Stelle 10 ist die Lenkung (1 Linkslenker), Stelle 11 das Werk; Beispiel WDD 169 007-1J-236589 in Wikipedia „Fahrzeug-Identifizierungsnummer“ (https://de.wikipedia.org/wiki/Fahrzeug-Identifizierungsnummer, abgerufen 2026-10-09). Am A 180 d (W177, Baujahr nach 2018) ergab sie sonst „2001“. Für diese WMI gibt es kein Modelljahr.

  • Plausibilität über das OBD-Protokoll (decode_vin(vin, protocol=...), nur wenn die FIN aus dem Fahrzeug gelesen wurde): Spricht das Fahrzeug ein OBD-II-Protokoll (SAE J1850, ISO 9141-2, ISO 14230-4, ISO 15765-4), sind Modelljahre vor 1994 unplausibel (die OBD-II-Spezifikation entstand um 1994, Pflicht in den USA ab Modelljahr 1996, EOBD in der EU ab 2001 Benziner/2004 Diesel). Bei CAN nach ISO 15765-4 sind Jahre vor 2000 unplausibel (CAN war für OBD-II in den USA erst ab Modelljahr 2003 zulässig; 2000 lässt Spielraum). Quelle: Wikipedia „On-board diagnostics“ (https://en.wikipedia.org/wiki/On-board_diagnostics, abgerufen 2026-10-07). Solche Jahre fallen aus den Alternativen; die beste Schätzung bleibt unverändert, sie ist ohnehin das jüngere Jahr. SAE J1939 (Nutzfahrzeuge) und unbekannte Protokolle schränken nichts ein.

.. py:class:: VinInfo(vin: str, valid: bool, checksum_ok: bool | None, wmi: str, manufacturer: str | None = None, country: str | None = None, model_year: int | None = None, online: dict[str, str] = , model_year_alternatives: tuple[int, …] = ()) :module: obd_diag.services.vehicle

Bases: :py:class:object

.. py:function:: model_year_text(info) :module: obd_diag.services.vehicle

z. B. „2026 oder 1996 (aus Stelle 10, ohne Gewähr)“; None ohne Modelljahr.

.. py:function:: checksum_text(info) :module: obd_diag.services.vehicle

Prüfziffer als Text, gleich in Kommandozeile, Bericht und Oberfläche.

„stimmt“ auch dort, wo sie nicht vorgeschrieben ist; „nicht vorgeschrieben“ nur, wenn sie nicht stimmt, das aber kein Fehler ist (z. B. in Europa).

.. py:function:: parse_vin_response(response) :module: obd_diag.services.vehicle

FIN aus der Antwort auf 0902 (CAN mehrteilig oder ältere Protokolle).

None, wenn die Antwort keine 17-stellige FIN enthält (z. B. nur Füllbytes oder abgelehnt); ValueError bei Zeilen, die keine Hex-Daten sind, und FrameSequenceError (Unterklasse) bei vermischten Frames mehrerer Steuergeräte (siehe split_messages).

.. py:function:: vin_from_messages(messages) :module: obd_diag.services.vehicle

Wie parse_vin_response, aber für bereits zerlegte Nachrichten.

.. py:function:: read_vin(elm) :module: obd_diag.services.vehicle

None, wenn das Fahrzeug Mode 09 PID 02 nicht unterstützt (vor ca. 2005).

Ebenso None (mit Log-Eintrag), wenn die Antwort keine gültig aussehende FIN enthält; ElmError, wenn sie sich gar nicht zerlegen lässt. Senden mehrere Steuergeräte ihre FIN gleichzeitig mehrteilig (ohne Header nicht zuzuordnen), wird die Anfrage einmal mit Headern (ATH1) wiederholt.

.. py:function:: check_digit(vin) :module: obd_diag.services.vehicle

Prüfziffer (0-9 oder X) einer gültigen FIN nach 49 CFR 565.15.

.. py:function:: earliest_plausible_year(protocol) :module: obd_diag.services.vehicle

Frühestes plausibles Modelljahr für ein Fahrzeug, das dieses Protokoll spricht.

protocol ist die Bezeichnung laut Adapter (ATDP, z. B. „ISO 15765-4 (CAN 11/500)“); None, leer, SAE J1939 oder unbekannt schränken nichts ein.

.. py:function:: model_years(vin, *, today=None, earliest=1980) :module: obd_diag.services.vehicle

Mögliche Modelljahre aus Stelle 10, beste Schätzung zuerst; () bei unbekanntem Code.

Nordamerika: nur das Jahr nach Stelle 7. Sonst das jüngste Jahr bis ein Jahr in der Zukunft und, falls nicht vor earliest, das 30 Jahre ältere.

.. py:function:: model_year(vin, *, today=None) :module: obd_diag.services.vehicle

Modelljahr (beste Schätzung) aus Stelle 10 einer gültigen FIN, None bei unbekanntem Code.

.. py:function:: decode_vin(vin, *, protocol=None, today=None) :module: obd_diag.services.vehicle

Rein offline: Gültigkeit, Prüfziffer, WMI-Hersteller/Land, Modelljahr.

protocol: OBD-Protokoll des Fahrzeugs, aus dem die FIN stammt (Bezeichnung laut Adapter); schließt unplausible ältere Modelljahre aus (siehe Moduldoku).

.. py:function:: default_vpic_cache_dir() :module: obd_diag.services.vehicle

$XDG_CACHE_HOME/obd-diag/vpic (Standard: ~/.cache/obd-diag/vpic).

.. py:function:: lookup_vpic(vin, *, cache_dir=None, timeout=5.0) :module: obd_diag.services.vehicle

Fragt NHTSA vPIC ab (nur aufrufen, wenn Nutzer zugestimmt hat).

Ergebnisse werden unter cache_dir (Standard: $XDG_CACHE_HOME/obd-diag/vpic) je FIN abgelegt; eine FIN ändert sich nie, ein Abruf pro Fahrzeug reicht. Netzwerkfehler ergeben ein leeres dict.

obd_diag.services.session

.. py:module:: obd_diag.services.session

Diagnosesitzung: alles, was ein Durchgang ausliest, plus Speichern und Export.

.. py:class:: Session(created: datetime.datetime, scan: obd_diag.services.diagnostics.ScanResult, readiness: obd_diag.services.readiness.ReadinessStatus | None = None, freeze_frame: obd_diag.protocol.obd.FreezeFrame | None = None, vehicle: obd_diag.services.vehicle.VinInfo | None = None, clear_counters: obd_diag.services.used_car.ClearCounters | None = None, ecus: tuple[obd_diag.protocol.vehicle_info.EcuInfo, …] | None = None, tests: tuple[obd_diag.protocol.monitor_tests.TestResult, …] | None = None) :module: obd_diag.services.session

Bases: :py:class:object

.. py:function:: run_diagnosis(elm, catalog, lang=None, *, online_vin_lookup=False, online_dtc_lookup=False) :module: obd_diag.services.session

Scan, Readiness, Freeze Frame, FIN, Zähler seit dem Löschen, Steuergeräte-Angaben (Mode 09) und Testergebnisse (Mode 06) in einem Durchgang; nur lesend.

Fehlt eine einzelne Angabe (Steuergerät antwortet nicht), bleibt sie None; nur ein fehlgeschlagener Scan bricht ab.

Der Scan setzt den Adapter zurück (ATZ) und handelt das Protokoll aus; danach folgen Readiness (0101), Freeze Frame (02xx00), FIN (0902), die Zähler für den Gebrauchtwagen-Check (0130, 0131, 014E, 0121, 014D), Mode 09 (0900 und die gemeldeten InfoTypes) und Mode 06 (0600 … und die gemeldeten MIDs), die beiden letzten mit Headern. Ein ElmError in einem dieser Teile lässt nur diesen Teil leer; Verbindungsfehler (TransportError) brechen ab. Der Freeze Frame wird immer gelesen, bleibt aber None, wenn er weder einen auslösenden Code noch Werte enthält (ohne gespeicherten Code ist er leer). Die FIN geht nur mit online_vin_lookup an NHTSA vPIC. Mit online_dtc_lookup bekommen Codes ohne Katalogtext eine Online-Erklärung (services/dtc_online; die FIN verlässt dabei nie den Rechner, nur der Hersteller entscheidet, welche Datei geladen wird).

.. py:function:: default_session_dir() :module: obd_diag.services.session

$XDG_DATA_HOME/obd-diag/sessions (Standard: ~/.local/share/obd-diag/sessions).

.. py:function:: session_to_dict(session) :module: obd_diag.services.session

JSON-taugliche Form; session_from_dict kehrt sie um.

Der Scan hat dieselbe Form wie bei obd-diag scan --json.

.. py:function:: session_from_dict(data) :module: obd_diag.services.session

Gegenstück zu session_to_dict; ValueError bei fremden oder kaputten Daten.

.. py:function:: save_session(session, directory=None) :module: obd_diag.services.session

Speichert als JSON mit Zeitstempel im Namen; überschreibt nie.

Dateiname: session-YYYYmmdd-HHMMSS.json nach session.created, bei Namensgleichheit mit -2, -3 usw.

.. py:function:: load_session(path) :module: obd_diag.services.session

Liest eine mit save_session gespeicherte Sitzung.

ValueError (Meldung in der eingestellten Sprache), wenn die Datei kein gültiges JSON oder keine obd-diag-Sitzung ist; OSError, wenn sie sich nicht lesen lässt.

obd_diag.services.compare

.. py:module:: obd_diag.services.compare

Sitzungsvergleich: was sich zwischen zwei gespeicherten Diagnosen geändert hat.

Arbeitet nur auf gespeicherten Daten (Session), spricht also nie mit dem Fahrzeug. Die ältere Sitzung ist „vorher“, die jüngere „nachher“ (nach Session.created).

Jeder Teil der Sitzung hat eine eigene Vergleichsfunktion in SECTIONS (Schlüssel, Überschrift, Funktion). Kommen neue Daten in die Sitzung (z. B. Mode 06 oder weitere PIDs), bekommen sie dort eine eigene Funktion; die Ausgabe in Kommandozeile und Oberfläche zeigt jeden Abschnitt ohne weitere Änderung an.

Texte entstehen beim Vergleich in der eingestellten Sprache (tr); nach einem Sprachwechsel einfach neu vergleichen. Fehlercode-Titel bleiben in der Sprache des Scans.

.. py:class:: ChangeKind(*values) :module: obd_diag.services.compare

Bases: :py:class:~enum.StrEnum

.. py:class:: Trend(*values) :module: obd_diag.services.compare

Bases: :py:class:~enum.StrEnum

.. py:class:: Change(kind: obd_diag.services.compare.ChangeKind, subject: str, before: str | None, after: str | None, trend: obd_diag.services.compare.Trend = <Trend.NEUTRAL: ‘neutral’>) :module: obd_diag.services.compare

Bases: :py:class:object

.. py:class:: Section(key: str, title: str, changes: tuple[obd_diag.services.compare.Change, …]) :module: obd_diag.services.compare

Bases: :py:class:object

.. py:property:: Section.differences :module: obd_diag.services.compare :type: tuple[~obd_diag.services.compare.Change, …]

  Alles außer ``UNCHANGED``.

.. py:class:: SessionComparison(before: obd_diag.services.session.Session, after: obd_diag.services.session.Session, sections: tuple[obd_diag.services.compare.Section, …], swapped: bool) :module: obd_diag.services.compare

Bases: :py:class:object

.. py:property:: SessionComparison.same_vehicle :module: obd_diag.services.compare :type: bool | None

  Gleiche FIN? ``None``, wenn eine der Sitzungen keine FIN hat.

.. py:function:: change_text(change) :module: obd_diag.services.compare

Wert einer Änderung als Text, z. B. „ausstehend → gespeichert“ oder „neu: gespeichert“.

.. py:function:: compare_sessions(first, second) :module: obd_diag.services.compare

Vergleicht zwei Sitzungen; die ältere gilt als „vorher“ (Reihenfolge egal).

.. py:function:: comparison_to_dict(comparison) :module: obd_diag.services.compare

JSON-taugliche Form (obd-diag compare --json); Texte in der eingestellten Sprache.

obd_diag.services.storage

.. py:module:: obd_diag.services.storage

Ablage unter $XDG_DATA_HOME/obd-diag (Sicherungen, Sitzungen, Mitschnitte).

.. py:function:: data_dir() :module: obd_diag.services.storage

$XDG_DATA_HOME/obd-diag (Standard: ~/.local/share/obd-diag).

.. py:function:: trace_dir() :module: obd_diag.services.storage

Ablage der Adapter-Mitschnitte (obd-diag ... --trace).

.. py:function:: write_new_json(directory, stem, data) :module: obd_diag.services.storage

Schreibt data atomar als <stem>.json (belegt: <stem>-2.json usw.).

Vorhandene Dateien werden nie überschrieben, und unter dem endgültigen Namen erscheint nur eine vollständig geschriebene Datei.

obd_diag.services.live

.. py:module:: obd_diag.services.live

Live-Daten (Mode 01) fortlaufend lesen und als CSV aufzeichnen; nur lesend.

Ablauf: prepare_live initialisiert den Adapter und ermittelt die unterstützten Werte, run_live fragt die gewählten Werte reihum ab, bis should_stop True liefert oder max_samples erreicht ist. Die Bordspannung wird regelmäßig gelesen; unter LOW_VOLTAGE wird die Abfrage gedrosselt.

Gesendet werden nur initialize/protocol (AT-Befehle, 0100), ATRV und die 01xx-Anfragen aus protocol.pids.

.. py:exception:: SelectionError :module: obd_diag.services.live

Bases: :py:class:ValueError

Die gewünschten Werte lassen sich nicht abfragen (unbekannt, nicht unterstützt oder leere Auswahl). Andere ValueError stammen aus kaputten Antworten.

.. py:class:: LiveSample(elapsed: float, values: dict[str, float | None], voltage: float | None, throttled: bool, alarms: tuple[obd_diag.services.alarms.Alarm, …] = ()) :module: obd_diag.services.live

Bases: :py:class:object

.. py:class:: LiveSetup(adapter: str, protocol: str, available: list[obd_diag.protocol.pids.PidSpec]) :module: obd_diag.services.live

Bases: :py:class:object

.. py:function:: prepare_live(elm) :module: obd_diag.services.live

Adapter initialisieren, Protokoll aushandeln, unterstützte Live-Werte ermitteln.

Befehle: initialize (ATZ … ATSP0), protocol (0100, ATDPN, ATDP), dann pids.read_supported_pids (0100, ggf. 0120 …). Fehler gehen an den Aufrufer.

.. py:function:: select_pids(setup, keys, *, skip_unsupported=False) :module: obd_diag.services.live

Die gewünschten Werte (None: DEFAULT_KEYS), soweit unterstützt.

Unbekannte Schlüssel ergeben SelectionError; bekannte, aber nicht unterstützte ebenfalls (Meldung nennt sie), damit der Nutzer nicht stumm leere Spalten bekommt. Bei None oder skip_unsupported (Auswahl getroffen, bevor bekannt war, was das Fahrzeug kann) werden nicht unterstützte Werte stillschweigend weggelassen. Reihenfolge: wie in keys bzw. DEFAULT_KEYS; doppelte Schlüssel einmal. Bleibt kein Wert übrig, ist das ebenfalls ein SelectionError: eine Abfrage ohne Werte liest nichts.

.. py:class:: LiveRecorder(path, pids) :module: obd_diag.services.live

Bases: :py:class:object

Schreibt Live-Werte als CSV (UTF-8 mit BOM; Trennzeichen und Dezimalzeichen nach der Sprache wie export: Deutsch Semikolon und Komma, Englisch Komma und Punkt).

Kopfzeile: Zeit (s), dann je Wert Name (Einheit), Bordspannung (V) und zuletzt Warnungen (in der eingestellten Sprache). Die Warnungen einer Runde stehen als Text in einer Zelle, mehrere durch | getrennt. Nicht lesbare Werte bleiben leere Zellen. Jede Zeile wird sofort geschrieben (flush), damit ein Abbruch nichts verliert. Die Datei wird beim Erzeugen exklusiv angelegt; existiert sie schon, gibt es FileExistsError (nie überschreiben).

.. py:method:: LiveRecorder.add(sample) :module: obd_diag.services.live

  Hängt eine Runde als Zeile an (sofort auf die Platte).

.. py:function:: alarm_text(alarm, pids) :module: obd_diag.services.live

Text einer Warnung in der eingestellten Sprache, Name und Einheit aus pids.

.. py:function:: recording_dir() :module: obd_diag.services.live

$XDG_DATA_HOME/obd-diag/recordings.

.. py:function:: new_recording_path(directory) :module: obd_diag.services.live

Freier Name live-YYYYmmdd-HHMMSS[-n].csv; legt den Ordner an.

.. py:function:: run_live(elm, pids, *, on_sample, should_stop, interval=1.0, max_samples=None, recorder=None, thresholds=None, clock=, sleep=) :module: obd_diag.services.live

Fragt pids reihum ab und meldet jede Runde über on_sample.

Erwartet einen mit prepare_live vorbereiteten Adapter. Jede PID wird je Runde einmal abgefragt, auch wenn mehrere Werte aus ihr stammen. Ein einzelner nicht lesbarer Wert (ElmError oder ValueError) ergibt None in der Runde (kein Abbruch); TransportError bricht ab. Scheitert in MAX_FAILED_ROUNDS Runden nacheinander jede Abfrage an einem Adapterfehler (Bus weg, Zündung aus), endet die Abfrage mit ElmError. should_stop wird vor jeder Runde und während des Wartens geprüft (in Schritten von höchstens 0,1 s).

Die Bordspannung wird in Runde 0 und jeder VOLTAGE_EVERY-ten Runde gelesen; liegt sie unter LOW_VOLTAGE, beginnen Runden nur alle LOW_VOLTAGE_INTERVAL Sekunden (throttled), und sie wird jede Runde gelesen, damit die Drosselung endet, sobald die Spannung wieder reicht (Motor angesprungen). Ist sie einmal nicht lesbar, bleibt die Drosselung, wie sie war (eine schwache Batterie wird davon nicht besser). Runden beginnen im Abstand interval ab Rundenbeginn; dauert eine Runde länger, folgt die nächste sofort. Mit thresholds (services.alarms) trägt jede Runde die überschrittenen Warnschwellen in LiveSample.alarms; gesendet wird dafür nichts. Liefert die Anzahl der Runden.

obd_diag.services.used_car

.. py:module:: obd_diag.services.used_car

Gebrauchtwagen-Check: Wurden die Fehlercodes vor kurzem gelöscht? Nur lesend.

Beim Löschen (Mode 04) setzt jedes Steuergerät nach SAE J1979 diese Zähler (Mode 01) auf null, und sie laufen danach mit dem Fahren wieder hoch:

  • PID 30: Warmläufe seit dem Löschen (0 bis 255; ein Warmlauf: Kühlmittel um mindestens 22 °C gestiegen und mindestens 70 °C erreicht)

  • PID 31: Strecke seit dem Löschen in km (0 bis 65535)

  • PID 4E: Zeit seit dem Löschen in Minuten (0 bis 65535)

Dazu zwei Zähler zur Motorkontrollleuchte (MIL), die beim Löschen ebenfalls auf null gehen: PID 21 (Strecke mit Warnleuchte an, km) und 4D (Zeit mit Warnleuchte an, Minuten). Permanente Codes (Mode 0A) lassen sich gar nicht löschen; sie kommen aus dem Scan, Warnleuchte und offene Tests aus der Readiness (PID 01).

Gefragt wird jede PID einmal, ohne vorher die unterstützten PIDs zu lesen (NO DATA heißt: nicht gemeldet). Antworten mehrere Steuergeräte, zählt bei den Zählern seit dem Löschen der kleinste Wert und bei den Warnleuchten-Zählern der größte: Ohne Header kommen die Antworten in wechselnder Reihenfolge (W177), und für die Frage „kürzlich gelöscht?“ zählt das Steuergerät, das es am deutlichsten zeigt.

Die Grenzen für „kürzlich“ (RECENT_*) sind eine Faustregel, keine Norm: Nach dem Löschen brauchen die Readiness-Tests meist einige Fahrten über ein paar hundert Kilometer, bis sie wieder abgeschlossen sind. Auch ein Batterie- oder Steuergerätewechsel und jede Werkstatt, die Codes löscht, setzen die Zähler zurück; ein Befund ist also ein Grund nachzufragen, kein Beweis.

.. py:class:: ClearCounters(warmups=None, distance_km=None, time_min=None, mil_distance_km=None, mil_time_min=None) :module: obd_diag.services.used_car

Bases: :py:class:object

Zähler seit dem letzten Löschen der Fehlercodes; None: nicht gemeldet.

.. py:property:: ClearCounters.available :module: obd_diag.services.used_car :type: bool

  Mindestens ein Zähler seit dem Löschen (30, 31, 4E) ist bekannt.

.. py:function:: read_clear_counters(elm) :module: obd_diag.services.used_car

Liest PID 30, 31, 4E, 21 und 4D (je eine Anfrage, in dieser Reihenfolge).

Ein Adapterfehler (ElmError) lässt nur diesen Zähler leer; Verbindungsfehler (TransportError) gehen an den Aufrufer. Nur nach ausgehandeltem Protokoll aufrufen.

.. py:class:: UsedCarReading(scan, readiness, counters) :module: obd_diag.services.used_car

Bases: :py:class:object

Was der Check braucht: Scan (permanente Codes), Readiness, Zähler.

.. py:function:: read_used_car(elm) :module: obd_diag.services.used_car

Nur der Gebrauchtwagen-Check (obd-diag used-car): Scan, 0101, Zähler.

Die vollständige Diagnose (run_diagnosis) liest dasselbe mit; ohne Katalog, denn der Check zeigt nur die Codes selbst.

.. py:class:: Verdict(*values) :module: obd_diag.services.used_car

Bases: :py:class:~enum.StrEnum

.. py:class:: Level(*values) :module: obd_diag.services.used_car

Bases: :py:class:~enum.StrEnum

.. py:class:: Finding(level: obd_diag.services.used_car.Level, text: str) :module: obd_diag.services.used_car

Bases: :py:class:object

.. py:class:: UsedCarCheck(verdict: obd_diag.services.used_car.Verdict, recently_cleared: bool, findings: tuple[obd_diag.services.used_car.Finding, …]) :module: obd_diag.services.used_car

Bases: :py:class:object

.. py:function:: counter_rows(counters) :module: obd_diag.services.used_car

Bezeichnung und Wert je Zähler in der eingestellten Sprache, auch „nicht gemeldet“.

.. py:function:: assess(counters, scan, readiness) :module: obd_diag.services.used_car

Bewertet Zähler, permanente Codes, Warnleuchte und offene Tests.

Texte in der eingestellten Sprache; bei einem Sprachwechsel neu aufrufen. counters ist None bei Sitzungen, in denen die Zähler nicht gelesen wurden.

obd_diag.services.battery

.. py:module:: obd_diag.services.battery

Batterie- und Anlasstest: Ruhespannung, Einbruch beim Anlassen, Ladespannung; nur lesend.

Ablauf (run_battery_test), der Nutzer startet den Motor selbst:

  1. Vorbereitung: initialize, protocol; Drehzahl (010C) muss 0 sein (Motor aus, Zündung an), Kühlmitteltemperatur (0105) für die Grenze beim Anlassen.

  2. Ruhe (REST_SECONDS): Bordspannung laut Adapter (ATRV) in schneller Folge, am Anfang und Ende die Steuergerätespannung (PID 42).

  3. Anlassen: Aufforderung, den Motor zu starten; bis START_TIMEOUT wird ATRV so schnell gelesen, wie der Adapter antwortet (höchstens alle SAMPLE_INTERVAL), und außerhalb des Einbruchs alle RPM_INTERVAL die Drehzahl. Ein Einbruch um mehr als CRANK_DROP unter die Ruhespannung gilt als Anlassen, ab RUNNING_RPM läuft der Motor.

  4. Laden: nach SETTLE_SECONDS Einschwingen CHARGE_SECONDS lang ATRV und PID 42.

Warum ATRV für den Einbruch: Adapter-Befehle erreichen das Fahrzeug nicht, für sie gilt die Mindestpause von 50 ms zwischen Anfragen ans Fahrzeug nicht (Elm327.command). Am FORScan-Adapter (ELM327-Nachbau) dauerte ATRV etwa 15 ms, PID 42 mit Pause über 100 ms; ein Anlassvorgang dauert oft unter einer Sekunde. Der Adapter misst aber an Pin 16 hinter einer Schutzdiode: Am Mercedes A 180 d (W177) zeigte er in Ruhe und bei laufendem Motor stets etwa 0,8 V weniger als das Motorsteuergerät. Die Differenz in Ruhe (offset) wird deshalb auf alle ATRV-Werte aufgeschlagen, sofern das Steuergerät PID 42 liefert. Ruhe- und Ladespannung kommen bevorzugt direkt aus PID 42.

Bricht die Spannung so tief ein, dass der Adapter neu startet (LV RESET oder seine Kennung statt einer Spannung), wird er neu initialisiert und das im Ergebnis vermerkt.

Gesendet werden nur initialize/protocol (AT-Befehle, 0100), ATRV, 010C, 0105 und 0142; alles lesend.

Schwellen (Bewertung in rest_verdict, crank_verdict, charge_verdict):

  • Ruhe: Eine 12-V-Bleibatterie hat in Ruhe etwa 12,6 V voll, 12,4 V zu drei Vierteln, 12,2 V halb geladen, unter 12,0 V ist sie weitgehend leer (übliche Ladezustandstabelle, z. B. Battery University BU-903). Mit Zündung an ziehen Steuergeräte Strom, die Spannung liegt etwas tiefer; daher gut ab REST_GOOD (12,2 V), darunter bis LOW_VOLTAGE (11,8 V, wie die Warnung der Diagnose) schwach, darunter entladen. Nach einer Fahrt liegt die Spannung durch Oberflächenladung eine Weile höher.

  • Anlassen: Beim klassischen Belastungstest (halber Kälteprüfstrom, 15 s) darf die Spannung bei 21 °C nicht unter 9,6 V fallen, bei Kälte etwas tiefer (Tabelle CRANK_LIMITS, wie in Werkstatthandbüchern nach BCI). Das gilt hier sinngemäß für den Tiefstwert beim Anlassen, ohne die ersten INRUSH_SKIP Sekunden (Einschaltstrom des Anlassers). Die Kühlmitteltemperatur steht für die Batterietemperatur; bei warmem Motor ist sie höher und die Grenze damit die strengste. Bis CRANK_MARGIN darüber: knapp.

  • Laden im Leerlauf: CHARGE_GOOD bis CHARGE_MAX gut; darüber lädt der Regler zu hoch. Zwischen CHARGE_MIN und CHARGE_GOOD niedrig: bei Fahrzeugen mit Batteriemanagement (Start-Stopp, geregelte Lichtmaschine) oft gewollt. Unter CHARGE_MIN (unter der Ruhespannung einer vollen Batterie) lädt die Lichtmaschine nicht.

Die Schwellen sind Richtwerte für 12-V-Bleibatterien (auch AGM/EFB), keine Herstellerangabe.

.. py:class:: Phase(*values) :module: obd_diag.services.battery

Bases: :py:class:~enum.StrEnum

.. py:class:: Rating(*values) :module: obd_diag.services.battery

Bases: :py:class:~enum.StrEnum

.. py:exception:: BatteryTestRefused :module: obd_diag.services.battery

Bases: :py:class:Exception

Der Test kann nicht laufen (Motor läuft, keine Spannung lesbar); nichts gemessen.

.. py:exception:: BatteryTestCancelled :module: obd_diag.services.battery

Bases: :py:class:Exception

Vom Nutzer abgebrochen (should_stop).

.. py:class:: BatteryProgress(phase: obd_diag.services.battery.Phase, elapsed: float, voltage: float | None, rpm: float | None, remaining: float | None) :module: obd_diag.services.battery

Bases: :py:class:object

.. py:class:: BatteryTestResult(adapter: str, rest_adapter: float | None, rest_ecu: float | None, offset: float, coolant_temp: float | None, crank_detected: bool, crank_minimum: float | None, crank_lowest: float | None, crank_seconds: float | None, started: bool, charge_adapter: float | None, charge_ecu: float | None, adapter_reset: bool, curve: tuple[tuple[float, float], …]) :module: obd_diag.services.battery

Bases: :py:class:object

.. py:property:: BatteryTestResult.rest_voltage :module: obd_diag.services.battery :type: float | None

  Ruhespannung: Steuergerät, sonst Adapter.

.. py:property:: BatteryTestResult.charge_voltage :module: obd_diag.services.battery :type: float | None

  Ladespannung: Steuergerät, sonst Adapter (korrigiert).

.. py:function:: crank_limit(coolant_temp) :module: obd_diag.services.battery

Mindestspannung beim Anlassen für die Temperatur; ohne Angabe die strengste.

.. py:function:: run_battery_test(elm, *, on_progress=None, should_stop=<function >, start_timeout=30.0, clock=, sleep=) :module: obd_diag.services.battery

Führt den Test durch (siehe Modul); der Nutzer startet den Motor auf Phase.START.

on_progress meldet Phasenwechsel sofort, sonst höchstens alle PROGRESS_INTERVAL. BatteryTestRefused: Motor läuft schon oder der Adapter liefert keine Spannung. BatteryTestCancelled: should_stop wurde True. TransportError (Adapter weg) geht durch, ebenso Fehler beim Initialisieren. Springt der Motor nicht an, endet der Test nach start_timeout ohne Ladespannung.

.. py:class:: Verdict(rating: obd_diag.services.battery.Rating, text: str) :module: obd_diag.services.battery

Bases: :py:class:object

.. py:function:: overall_rating(result) :module: obd_diag.services.battery

Schlechteste der drei Bewertungen; UNKNOWN nur, wenn keine bewertet ist.

.. py:function:: notes(result) :module: obd_diag.services.battery

Hinweise zur Messung (in der eingestellten Sprache).

.. py:function:: result_to_dict(result) :module: obd_diag.services.battery

Für obd-diag battery --json; Schlüssel englisch wie bei den anderen Befehlen.