Abfrage von Fahrzeugwerten beim Ora Funky Cat über MeatPi WiCan-OBD-C3 und MQTT

Begonnen von tupol, 14 April 2024, 19:12:06

Vorheriges Thema - Nächstes Thema

tupol

Hallo,

ich habe seit ein paar Monaten einen Ora Funky Cat von GWM im Hof stehen. Da ich auch Solar habe, habe ich mir einen MeatPi WICAN-OBD-C3 Dongle zugelegt, um Werte wie den Batterie-Ladezustand und die Kilometer auszulesen und in meinem HEMS zu nutzen. Der Dongle greift über WLAN auf einen MQTT Server zu und fragt die Fahrzeugdaten über die OBD2-Schnittstelle des Fahrzeuges ab. Bei Interesse könnte ich die Konfiguration auch in einem Wiki Beitrag archivieren.

Meine Frage an die Community:
1. Hat das schon irgendjemand anderes am Laufen?
2. Wie trigger ich am besten das Auslesen über den MQTT Server? D.h. wie setze ich am besten regelmäßig das Publish ab? Gibt es dafür eine eingebaute Funktion oder muss ich einen Timer (at) setzen?

Gruß
Tupol

Otto123

Hallo Tupol,

  • ich nicht. Ich habe aber mal die Anleitung für HAS gelesen.
  • der MQTT Server ist ja erstmal die Drehscheibe. Du baust auf FHEM Seite noch ein MQTT2_Device (hast Du schon?), das bildet die Daten ab. Dort baust Du dir get Befehle (publish zum Auto) ein, je nach dem wie granular Du Werte abfragen kannst und willst. Die get Befehle triggerst Du dann mit einem Timer Deiner Wahl.

Ich denke das wird ein MQTT Thema, Du solltest das verschieben ;) und Deine MQTT Konfiguration bisher beschreiben.
Die MQTT Schnittstelle ist ja auf dem github halbwegs dokumentiert.

Gruß Otto
Viele Grüße aus Leipzig  ⇉  nächster Stammtisch an der Lindennaundorfer Mühle
RaspberryPi B B+ B2 B3 B3+ ZeroW,HMLAN,HMUART,Homematic,Fritz!Box 7590,WRT3200ACS-OpenWrt,Sonos,VU+,Arduino nano,ESP8266,MQTT,Zigbee,deconz

tupol

Hallo Otto123,

vielen Dank für den Hinweis mt dem Thema. Ich hatte es übersehen bzw. mich nach einem bereits existierenden Beitrag gerichtet. Ich kann leider den Beitrag nicht umziehen, habe aber die Administratoren informiert.

Was ist den ein HAS?

Ich habe das MQTT2-Device schon am laufen. Allerdings hatte ich mich gefragt, ob es eine automatische Abfrage ohne Timer gibt.

Gruß
Tupol

Otto123

Guten Morgen,

doch Du kannst das: unten Links gibt es den Knopf Thema verschieben...

HAS - Home Assistant Server, der quasi MQTT als Kern hat - dein Text war voll mit mir unbekannten Kürzeln, den konnte ich mir nicht verkneifen. :)

Automatische Abfragen gibt es beim MQTT2_Device nicht (im HA übrigens auch nicht). Üblicherweise liefern die Geräte bei Änderungen oder zeitgesteuert ihre Werte beim MQTT Server ab. Meistens mehr als einem lieb ist.
Hier ist das ganze mal andersrum (wegen Energieersparnis?) hat für Dich den Vorteil Du kannst bestimmen, welche Werte Du wann haben willst.
Aber Timer sind ja genügend da, bzw. kannst Du ja auch situationsbedingt Abfragen starten: Solarüberschuss -> steht das Auto da und verträgt die Batterie noch etwas Ladung?

Gruß Otto
Viele Grüße aus Leipzig  ⇉  nächster Stammtisch an der Lindennaundorfer Mühle
RaspberryPi B B+ B2 B3 B3+ ZeroW,HMLAN,HMUART,Homematic,Fritz!Box 7590,WRT3200ACS-OpenWrt,Sonos,VU+,Arduino nano,ESP8266,MQTT,Zigbee,deconz

tupol

Der WiCan von MeatPi ist ein recht clevere Möglichkeit, um über OBD2/CAN-Bus an Fahrzeugdaten ranzukommen. Bei den meisten E-Autos gibt es ja keine API, um auf die Batteriedaten oder die Laufleistung zugreifen zu können.
Natürlich kann man ihn auch bei Verbrenner-Autos nutzen, um z.B. die monatlichen Fahrstrecken zu tracken oder rechtzeitig mitzubekommen, wenn die Starter-Batterie leer ist.
Wundert mich, dass bisher außer einem Eintrag weder im Forum noch in der Wiki was dazu zu finden ist.

beaune

Hallo,

ja ich nutze den WiCan seit geraumer Zeit, um meinem Zoe damit auszulesen. Das Problem ist, dass man nicht immer an die Daten heran kommt und damit eine rein zyklische Abfrage nicht funktioniert. Man kann während des Ladens Requests stellen; das ist kein Problem, wenn man die Info von der Wallbox hat. Oder wenn man gerade nach Hause gekommen ist. Dafür muß man sich benachrichtigen lassen, wenn sich der Adapter gerade wieder mit dem Heim-Wifi verbunden hat, und hat dann ein paar Sekunden Zeit. Es geht auch manches, wenn das Auto zwar an der Wallbox hängt, ohne zu laden. Das ist dann so eien Art "Dämmerzustand", wo man einige Informationen, aber nicht alle, abrufen kann, nachdem man eine Art Wakeup-Telegramm geschickt hat. Der Zoe sendet einige Dinge auch automatisch, wenn man ihn startet oder abstellt. Das ist aber alles individuell, sowohl was das Verhalten des Autos angeht, aber auch was man wirklich wissen möchte.

Es ist übrigens eine neue Version der WiCan Firmware angekündigt, mit der man den Spannungspegel der 12V-Versorgung direkt ermitteln kann, ohne dass das Auto wach sein muß. Könnte auch ne interessante Information sein.

Guß
Beaune

tupol

Hallo Beaune,

ich bin bisher recht zufrieden mit der Auswertung (SoC, SoH, True SoC, Mileage, TotalCharge, TotalDischarge, BatteryVoltage, OperationalVoltage), aber immer noch am Forschen.
Was mich etwas irritiert, ist der Status (online/offline) des WiCAN auf dem MQTT-Server (Mosquitto). Ich dachte anfänglich, dass der vom WiCAN gesetzt wird, aber er setzt sich auf "offline", wenn ich wegfahre und der WiCAN somit nicht mehr im WLAN ist. Ist das eventuell ein Reading vom Server, das anzeigt, dass der WiCAN eingeloggt ist? Dann kann man ja die Abfrage minütlich laufen lassen, solange er online ist.

Hast Du auch Werte, die über zwei gestaffelte Publishes (tx-rx-Handshake + tx-rx zum Auslesen) abgefragt werden müssen? Bei den Reifendrucksensoren ist das anscheinend nötig aber ich weiß nicht so richtig, wie man das in das MQTT-Device integrieren kann.

Gruß
Tupol

beaune

Das mit dem status Reading ist mir noch gar nicht so aufgefallen. Du hast recht, das kann nur vom MQTT-Server gesendet werden (bei mir fhem), nicht vom Adapter. Wie genau hier der Informationsfluß vom Adapter zum Server ist, weiß ich auch nicht. Aber Tatsache ist, dass dieses Reading zuverlässig anzeigt, ob das Auto da ist oder nicht, und wenn Du da ein notify dran bindest, das ein at aktiviert/deaktiviert, dann geht das erstmal. Du mußt aber trotzdem davon ausgehen, dass das Auto irgendwann in sleep geht und dann keine Telegramme mehr kommen. Ich mache die zyklische Abfrage deshalb eben auch vom Verbindungsstatus der Wallbox abhängig, und frage ansonsten nur einmal. SoC, SoH etc. ändert sich ja auch nicht so schnell, wenn das Auto nur steht.

Was für mich anfangs etwas schwierig war, war die Bestimmung des Protokolls auf Layer 7. CAN ist ja nur Layer 2. Beim Zoe wird Unified Diagnostic Service UDS (29-Bit-Identifier) verwendet. Ich weiß nicht, ob das bei Deinem Fahrzeug auch so ist. Beim UDS gibt es einfache Parameteranfragen anhand einer Parameternummer. Es kann dann sein, dass als Antwort mehrere Telegramme kommen. Das hab ich zwar strukturell vorgesehen, aber auch noch nicht implementiert. Alles, was mich inhaltlich interessierte, war als Einzeltelegramm zugreifbar. Der Zoe schickt zwar manchmal von selbst solche Telegrammblöcke, die habe ich bislang aber einfach ignoriert. Der Blockdatentransfer ist sicher auch dann sinnvoll, wenn man sehr viele Daten lesen möchte. Ich brauchte bislang nur wenige Daten, daher hab ich mir die Mühe noch nicht gemacht. Aber das scheint eh nicht das zu sein, was Du oben mit tx/rx/handshake meinst.

tupol

Habe die Konfiguration hier in der Wiki veröffentlicht. Kann gerne verbessert werden.

beaune

Hallo,

dann ergänze ich hier mal das, was ich für den Zoe gebaut habe. Ich bin etwas anders vorgegangen und benutze nicht die Readings, die den Telegramminhalt einzeln darstellen, sondern nutze so etwas ähnliches wie json2nameValue in der ReadingList, um die ,,richtigen" Readings zu definieren. Das Ganze ist relativ generisch gestaltet; die zu lesenden Parameter definiert man einfach über ein Attribut. Ich hab mich allerdings hier auf 29-Bit-Identifier und das Das UDS-Protokoll (Unified Diagnostic Service, ISO14229-3) beschränkt. Für 11-Bit müßte man da sicher nochmal Hand anlegen, das Prinzip geht aber auch.

Die eigentliche Definition ist dabei noch relativ übersichtlich:

defmod MQTT2_ESP32_934C2C MQTT2_DEVICE ESP32_934C2C
attr MQTT2_ESP32_934C2C userattr CanObjectMap:textField-long event-on-update-reading:textField-long userattr:textField-long event-on-change-reading:textField-long

attr MQTT2_ESP32_934C2C event-on-change-reading .*
attr MQTT2_ESP32_934C2C getList SoC:noArg SoC { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","SoC","CanObjectMap") }\
RealCharge:noArg RealCharge { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","RealCharge","CanObjectMap") }\
UsableCharge:noArg UsableCharge { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","UsableCharge","CanObjectMap") }\
AccuVitalitySOH:noArg AccuVitalitySOH { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","AccuVitalitySOH","CanObjectMap") }\
Voltage12V:noArg Voltage12V { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","Voltage12V","CanObjectMap") }\
Akkuspannung:noArg Akkuspannung { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","Akkuspannung","CanObjectMap") }\
BatteryMileage:noArg BatteryMileage { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","BatteryMileage","CanObjectMap") }\
AvBattTemperature:noArg AvBattTemperature { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","AvBattTemperature","CanObjectMap") }\
LowVoltageSupply:noArg LowVoltageSupply { CANnameValue2JSON ($NAME,"wican/dc5475934c2d/can/tx","LowVoltageSupply","CanObjectMap") }
attr MQTT2_ESP32_934C2C readingList ESP32_934C2C:wican/dc5475934c2d/status:.* { json2nameValue($EVENT) }\
ESP32_934C2C:wican/dc5475934c2d/can/rx:.* rx_json\
ESP32_934C2C:wican/dc5475934c2d/can/rx:.*  { json2CANnameValue($NAME,$EVENT,AttrVal ($NAME,'CanObjectMap',0)) }
attr MQTT2_ESP32_934C2C room Energie,MQTT2_DEVICE
attr MQTT2_ESP32_934C2C setList InitDefaultSession:DB,DA,DC,DD,DE,DF { InitOBDDefaultDiagnosticSession ($NAME,"wican/dc5475934c2d/can/tx",$EVTPART1) }\
WakeUp:DB,DA { WakeUpECU($NAME,"wican/dc5475934c2d/can/tx",$EVTPART1) }
attr MQTT2_ESP32_934C2C stateFormat {\
  sprintf("zu Hause: %s <br>\
    SoC: %d %%<br>\
    SoC (real): %d %%<br>\
    SoC (usable): %d %%<br>\
    ",\
    (ReadingsVal($name,"status","") eq "online") ? "ja" : "nein",\
    ReadingsNum($name,"SoC",""),\
    int(ReadingsNum($name,"RealCharge","")+0.5),\
    int(ReadingsNum($name,"UsableCharge","")+0.5))\
}

Dazu kommt noch das Attribut zur Definition der Parameter. Das ist ganz individuell für jeden Autotyp zu definieren. Die Daten sind so zu interpretieren:

<CAN-Identifier>:<SID>:<DID><space><ReadingsName>:<Offset>:<Multiplier>:<Anzahl Nachkommastellen>
Beim Zoe zweiter Generation funktioniert z.B. das:

attr MQTT2_ESP32_934C2C CanObjectMap 18DAF1DB:62:92C1 SoC:0:1:0\
18DAF1DB:62:9018 maxChargeRekuperation:0:1:2\
18DAF1DB:62:9002 UsableCharge:0:1:2\
18DAF1DB:62:9001 RealCharge:-300:1:2\
18DAF1DB:62:9003 AccuVitalitySOH:0:1:2\
18DAF1DB:62:9011 LowVoltageSupply:0:0.000976563:3\
18DAF1DB:62:9012 AvBattTemperature:640:0.0625:2\
18DAF1DB:62:91CF BatteryMileage:-2147483648:0.03125:2\
18DAF1DB:62:9243 CumulatedCharge:0:0.001:3\
18DAF1DB:62:9245 CumulatedDischarge:0:0.001:3\
18DAF1DB:62:9005 Akkuspannung:0:1:1\
18DAF1DA:62:2005 Voltage12V:0:1:2\

Damit das alles funktioniert, braucht man noch die Funktion zur Interpretation der UDS-Telegramme. Die Idee dabei ist, erstmal alles, was so an Telegrammen ankommt, zu interpretieren und ein Reading dafür anzulegen. Sind für einen so erfassten Roh-Parameterwert die zur Interpretation nötigen Daten gemäß des obenstehenden Attributs definiert, wird anstatt dessen ein entsprechendes ,,richtiges" Reading angelegt. ein Das habe ich als Funktion in 99_myUtils.pm implementiert:

sub json2CANnameValue ($$;$) {
  my ($name, $in, $map) = @_;
  return if(!$in);

  my $hash = $defs{$name};
  my $text = decode_json($in);
  my $CanDataLength = $text-> {frame}[0] -> {dlc};
  my $CanID = $text-> {frame}[0] -> {id};

  # Bestimmung der PCI-Byte-Inhalte
  my $FrameType = ($text-> {frame}[0] -> {data}[0] & 0xF0) >> 4;
  my $FrameTypeName = ($FrameType == 0) ? "Single" : ($FrameType == 1) ? "First" : ($FrameType == 2) ? "Consecutive" : ($FrameType == 3) ? "FlowControl" : $FrameType;

  my $SID = 0;
  my $DID = 0;
  my $DataLength = 0;
  my $SequenceNumber = 0;
  my $BlockSize = 0;
  my $SeparationTime = 0;
  my $FlowStatus = 0;
  my $DataStart = 0;
  my $ReadingValue = "";
  my @ReadingVal = "";

  # CAN-Objekt könnte unbekannt sein, als Default Reading mit ID publizieren
  my $ReadingName = "CAN_Object_" . sprintf("%X",$CanID)."_Frame_". $FrameTypeName;
  if ($FrameType == 0) {                       # Single Frame
    $DataLength  = $text-> {frame}[0] -> {data}[0] & 0x0F;
    $SID = $text-> {frame}[0] -> {data}[1];    # Bestimmung der Service-ID
    $DID = (($text-> {frame}[0] -> {data}[2]) << 8) + ($text-> {frame}[0] -> {data}[3]);  # Bestimmung der Parameter-ID(s)

    $DataStart = 4;
    $ReadingName .= "_DL_". $DataLength ."_SID_".sprintf("%02X",$SID)."_DID_".sprintf("%04X",$DID);
    for (my $i = $DataStart; $i < $CanDataLength; $i++) {
      $ReadingValue .= sprintf("%02x",$text-> {frame}[0] -> {data}[$i]);
      $ReadingValue .= '-';
    }
  } elsif ($FrameType == 1) {                  # First Frame
  } elsif ($FrameType == 2) {                  # Consecutive Frame
  }


  # Erkennen von Sondertelegrammen
  if (($FrameType == 0) && ($SID == 0x50) && (($text-> {frame}[0] -> {data}[2])) == 1) {
    Log 1,"Diagnosesession gestartet";
    $hash->{sprintf("%X",$CanID)."TimingP2"} = (($text-> {frame}[0] -> {data}[3]) << 8) + ($text-> {frame}[0] -> {data}[4]);
    $hash->{sprintf("%X",$CanID)."TimingP2x"} = (($text-> {frame}[0] -> {data}[5]) << 8) + ($text-> {frame}[0] -> {data}[6]);
  }

  # Berechnung der Dezimalwerte im Single Frame
  my $CalcValue = 0;
  for (my $i=$DataStart ; $i <= (($DataLength > $CanDataLength) ? $CanDataLength : $DataLength); $i++) {
    $ReadingValue .= "Wert hex: " . sprintf("%02x",$text-> {frame}[0] -> {data}[$i]);
    $CalcValue += ($text-> {frame}[0] -> {data}[$i]);
    if ($i < ($DataLength)) {
      $ReadingValue .= '-';
      $CalcValue = ($CalcValue << 8);
    };
  }
  $ReadingValue .= " dezimal: " . sprintf("%d",$CalcValue);

  # Suchen, ob für diese ID ein Name bzw. ein Format bekannt ist.
  if (defined($map)) {
    my @mapArray = split(" ",$map);
    my %mapHash = @mapArray;
    my $qualifier = sprintf("%4X:%X:%X",$CanID,$SID,$DID);

    if (defined($mapHash{$qualifier})) {
      my $name = (split(/:/,($mapHash{$qualifier})))[0];
      my $offset = (split(/:/,($mapHash{$qualifier})))[1];
      my $multiplier = (split(/:/,($mapHash{$qualifier})))[2];
      my $numberDigits = (split(/:/,($mapHash{$qualifier})))[3];

      # CAN-Objekt ist bekannt, also entsprechend benanntes Reading setzen und Wert formatieren
      $ReadingName = $name;
      $CalcValue = ($CalcValue + $offset) * $multiplier;
      if ($numberDigits == 1) {
        $ReadingValue = sprintf("%.1f",$CalcValue / 10);    
      } elsif ($numberDigits == 2) {
        $ReadingValue = sprintf("%.2f",$CalcValue / 100);    
      } elsif ($numberDigits == 5) {
        $ReadingValue = sprintf("%.2f",$CalcValue / 100000);    
      } else {
        $ReadingValue = $CalcValue;
      }
  return {@ReadingsDefs};
}

Was hier fehlt, ist die Auswertung von Blockdatentransfer (First Frame / Consecutive Frame). Man könnte so vorgehen, dass man die eintreffenden Telegramme aufgrund der Informationen im First Frame erstmal in einem Internal zwischen speichert, bzw. die Nutzdaten davon zu einem Riesen-Frame konkateniert, und dann wenn man erkannt hat, dass der Frame vollständig ist die Auswertung startet. Das hab ich bislang nicht gebraucht und daher nicht implementiert.

Damit hat man schon mal alles im Sack, was das Auto so von sich aus sendet, z.B. während es geladen wird. Wenn man jetzt noch gezielt Werte lesen möchte , kann man dies über die getList definieren. Das klappt allerdings nur, wenn das Auto gerade nicht schläft. Das festzustellen oder es ,,aufzuwecken" ist wieder individuell. Die obenstehende Definition benötigt auch eine Funktion  in 99_myUtils.pm:


sub CANnameValue2JSON ($$$$) {
  my ($name,$topic,$qualifier, $mapname) = @_;
  my $hash = $defs{$name};
  my $map = AttrVal ($name,$mapname,0);
  my $offset;
  my $multiplier;
  my $numberDigits;
  my $CanID;
  my $SID;
  my $DID;
 
  no warnings 'uninitialized';
  if (ReadingsVal($name,"status","") ne "online") { return undef };

  # CAN-Daten für diesen Parameter raussuchen
  if (defined($map)) {
    my @mapArray = split(" ",$map);
    my $max = @mapArray;
    for (my $i=0; $i<$max; $i++) {
      my @data = split(/:/,$mapArray[$i+1]);
      if ($data[0] eq $qualifier) {
        $offset = $data[1];
        $multiplier = $data[2];
        $numberDigits = $data[3];
        @data = split(/:/,$mapArray[$i]);
        $CanID = $data[0];
        $SID = $data[1];
        $DID = $data[2];
      }
    }
  }

  my ($Byte1,$Byte2,$Byte3,$Byte4) = unpack("v4",$CanID);
  $CanID = pack("v4",$Byte1,$Byte2,$Byte4,$Byte3);
  my @CanData = (0,0,0,0,0,0,0,0); # oder doch 170?
  $CanData[0] = 3; # Single Frame ohne Nutzdaten, wobei 3 Bytes für DID und SID benötigt werden
  $CanData[1] = hex($SID) - 0x40;
  $CanData[2] = (hex($DID) & 0xFF00) >> 8;
  $CanData[3] = (hex($DID) & 0x00FF);

  my $json = '{"bus":"0","type":"tx","frame":[{"id":';
  $json .= hex($CanID) .',"dlc":8,"rtr":false,"extd":true,"data":[';
  for (my $j=0; $j<8; $j++) {
    $json .= $CanData[$j];
    if ($j < 7) { $json .= ","}
  }
  $json .= "]}]}";

 # Sleep-Zustand beachten
  my $timeDifference = time - $hash->{"LastTimestamp"};
  if ((ReadingsAge($name,"rx_json",0) > 600) && ($timeDifference > 10)){
    # Auto schläft wahrscheinlich
    Log 1,"Auto schläft wahrscheinlich, also aufwecken; Zeitdifferenz: $timeDifference ";
    fhem("sleep 0.1; set $name WakeUp DB");
    fhem("sleep 0.5; get $name $qualifier");
    return WakeUpECU ($name,$topic,"DA");
  }
  return "$topic $json";
}

Ich hab dann noch Funktionen implementiert, um eine Diagnosesession neu zu starten oder die Zoe aufzuwecken, das mag aber bei anderen Autotypen auch anders sein. Der Vollständigkeit halber füge ich auch das ein:

sub InitOBDDefaultDiagnosticSession ($$$) {
  my ($name,$topic,$ECU) = @_;
  my $hash = $defs{$name};
  my $CanID = "18DA" . $ECU . "F1";

  my $json = '{"bus":"0","type":"tx","frame":[{"id":'. hex($CanID) .',"dlc":8,"rtr":false,"extd":true,"data":[2,16,1,0,0,0,0,0]}]}';
  return "$topic $json";
}

sub WakeUpECU ($$$) {
  my ($name,$topic,$ECU) = @_;
  my $hash = $defs{$name};
  my $CanID = "18DA" . $ECU . "F1";

  my $json = '{"bus":"0","type":"tx","frame":[{"id":'. hex($CanID)
    .',"dlc":8,"rtr":false,"extd":true,"data":[2,1,0,170, 170, 170, 170, 170]}]}';
  return "$topic $json";
}

Ist etwas umfangreicher, als Dein Ansatz, aber vielleicht auch etwas generischer und strukturell auch für die Auswertung von Blockdaten vorgesehen. Ausbaubar ist das sicher auch noch und bestimmt auch nicht optimal programmiert. Aber vielleicht hilfts so ja schon irgendjemandem.


tupol

Wau. Hut ab!! Schöner generischer Ansatz.
Ich vermute, das braucht einen Wiki-Eintrag, um von anderen verwendet zu werden. :-) Vielleicht auch gleich ein passendes pm-Modul. ;)
Was noch interessant wäre, ist ein Ansatz für die Kommunikation mit zwei TX/RX Abfragen.

Leider sendet der ORA nix von allein oder ich habe irgendwas falsch gemacht.