Teltonika FMC003: Live-Daten via OBD-Dongle

So, zwei geeignete OBD-Stecker, zwei SIM-Karten für etwas Datentraffic und eine Instanz Nodered und schon erhalten meine zwei Wallboxen die SoCs direkt aus den Autos angeliefert.

Die Vorteile:

  • keine fremden Hops mehr, die man nicht im Griff hat
  • Daten sind aktuell

Die Nachteile

  • Zusätzliche Investition für OBD-Stecker
  • Je nach Anbieter laufende Kosten für die SIM-Karten
  • nur noch eigene Hops, die gepflegt werden wollen :wink:
  • Funktioniert bei meinen Fahrzeugen leider nicht im Stand, SoC wird nur während der Fahrt übertragen, d.h. der Ladevorgang muss anders abgebildet werden. Meine openWBs bieten diese Möglichkeit.

Die Liste der Nachteile ist zwar länger, ich bin mit dieser Lösung aber sehr zufrieden.

Welchen OBD-Stecker nutzt du?
Wie kommst du an die CAN-Botschaft des SoC?
Welchen anderen Werte liegen (lesbar) auf dem CAN?
Kannst du deinen Code veröffentlichen?

Danke.

Ich nutze den FMC003 von Teltonika, der funktioniert sowohl mit dem P2 als auch mit dem e208. Das Teil kann man via USB oder BT so konfigurieren, dass er selbst generierte Werte wie bspw. GPS-Daten, Batteriespannung oder Beschleunigung, aber auch OBD-Werte als Datenpaket an einen (oder auch zwei verschiedene) Server verschickt.

Welche Werte das sein können, findet man hier. Der P2 unterstützt mindestens SoC, SoH und Odometer, leider aber nur während der Fahrt.

Meinen Code kann ich veröffentlichen, wenn ich wieder dran komme (bin gerade auf Reisen), den relevanten Teil extrahiert und anonymisiert habe. Gib mir mal bis zum Wochenende. Den Kern des Codes (Handshake und Parser) habe ich allerdings hier ausgeliehen, vielleicht reicht das schon als Anregung. Das Protokoll ist aber auch detailliert hier beschrieben.

2 „Gefällt mir“

Das Thema ist so interessant - und so weit weg von Tibber, dass ich es in einen neuen Thread geschoben habe. Vorschläge zum Titel gerne!

Ausgangspunkt war:

Hier wird es richtig spannend! :sunglasses: :sunglasses: :sunglasses: :sunglasses: :sunglasses: :sunglasses:

Das wäre super.

Vielleicht am besten auf GitHub?

Die Synthese aus der Polestar-API und dem Dongle wäre natürlich perfekt:

  • Detaillierte Live-Daten während der Fahrt
  • SoC und Ladestatus beim Parken

So, hier habe ich euch mal die Minimalkonfiguration zusammengestellt:

… oder zum Importieren in NodeRed (sorry, GitHub ist Neuland für mich):

[
    {
        "id": "1c5b57320a0e5fd6",
        "type": "tab",
        "label": "Polestar SoC from FMC003",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "f6f7ac8c3e9cc4b1",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "RAW data",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1000,
        "y": 260,
        "wires": []
    },
    {
        "id": "fa702d0bc584f592",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "Filter PING packet",
        "func": "if (msg.payload.length == 1) {\n    msg.payload = \"PING. \" + msg.ip + \":\" + msg.port;\n    return [null, msg];\n}\n\nreturn [msg, null];",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 850,
        "y": 320,
        "wires": [
            [
                "f6f7ac8c3e9cc4b1",
                "9d0b1c897bac8bf3"
            ],
            [
                "ad5d5bd57d71a025"
            ]
        ]
    },
    {
        "id": "ad5d5bd57d71a025",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "PING packets",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1020,
        "y": 380,
        "wires": []
    },
    {
        "id": "9d0b1c897bac8bf3",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "PARSE codec8_ext TCP",
        "func": "var inputMsg = msg.payload;\n\nvar inputDataPtr = 0;\nvar fullPacketLength = 0;\n\nvar parsedMsg = {};\nparsedMsg.avl = [];\nvar avl = {};\n\nparseAVLfull();\n\n// create TCP response\nvar tcpResponse = createTcpResponse(parsedMsg.numberOfData, msg._session);\n\nmsg.payload = parsedMsg;\n\nreturn [msg, tcpResponse];  // response\n\n//**********************************************************************************************\n// createTcpResponse\n//**********************************************************************************************\nfunction createTcpResponse(receivedDataCount, session) {\n    var countHex = dec2Hex32(receivedDataCount);\n    var tcpResponse = {};\n    tcpResponse._session = session;\n    tcpResponse.payload = new Buffer([Number(countHex.substring(6, 8)), Number(countHex.substring(4, 6)),\n    Number(countHex.substring(2, 4)), Number(countHex.substring(0, 2))]);\n\n    return tcpResponse;\n}\n\n//**********************************************************************************************\n// dec2Hex32\n//**********************************************************************************************\nfunction dec2Hex32(dec) {\n    return Math.abs(dec).toString(32);\n}\n\n//**********************************************************************************************\n// parseAVLfull\n//**********************************************************************************************\nfunction parseAVLfull() {\n    //preamble\n    var preamble = getNextInt32();\n    if (preamble != 0x00000000) {\n        node.warn(\"Wrong preamble\");\n        return [null, null];\n    }\n\n    // get packet length\n    parsedMsg.fullPacketLength = getNextInt32();\n\n    var calculatedCrc16 = crc16(inputMsg, inputDataPtr, (inputMsg.length - inputDataPtr - 4));\n\n    // AVL codec ID\n    parsedMsg.codecId = getNextInt8();\n    if (parsedMsg.codecId != 0x8E) {\n        node.warn(\"Wrong codecId: \" + parsedMsg.codecId);\n        return [null, null];\n    }\n\n    // AVL number of data\n    parsedMsg.numberOfData = getNextInt8();\n\n    // AVL Data array    \n\n    for (var i = 0; i < parsedMsg.numberOfData; i++) {\n        parseAVLpacketDataArrayElement();\n    }\n\n    parsedMsg.numberOfRecords = getNextInt8();\n\n    if (parsedMsg.numberOfData != parsedMsg.numberOfRecords) {\n        node.warn(\"numberOfData1 not equals numberOfData2\");\n        return [null, null];\n    }\n\n    parsedMsg.crc16 = getNextInt32();\n    if (calculatedCrc16 != parsedMsg.crc16) {\n        node.warn(\"Wrong crc16.\");\n        return [null, null];\n    }\n}\n\n//**********************************************************************************************\n// parseAVLpacketDataArrayElement\n//**********************************************************************************************\nfunction parseAVLpacketDataArrayElement() {\n    avl = {};\n    avl.timestamp = getTimestamp();\n    avl.priority = getNextInt8();\n    avl.GPS = parseGPSelement();\n    parseIOelement();\n    parsedMsg.avl.push(avl);\n}\n\n//**********************************************************************************************\n// parseAVLpacket\n//**********************************************************************************************\nfunction getTimestamp() {\n    var timestampArray = getNextSubArray(8);\n    var value = 0;\n    for (var i = 0; i < timestampArray.length; i++) {\n        value = (value * 256) + timestampArray[i];\n    }\n    return value;\n}\n\n//**********************************************************************************************\n// parseGPSelement\n//**********************************************************************************************\nfunction parseGPSelement() {\n    var GPS = {};\n    GPS.longitude = getCoordinate(getNextSubArray(4));\n    GPS.latitude = getCoordinate(getNextSubArray(4));\n    GPS.altitude = getNextInt16();\n    GPS.angle = getNextInt16();\n    GPS.satellites = getNextInt8();\n    GPS.speed = getNextInt16();\n\n    return GPS;\n}\n\n//**********************************************************************************************\n// getCoordinate\n//**********************************************************************************************\nfunction getCoordinate(array) {\n    var value = 0;\n    if (array[0] > 127) { //negative\n        value = (array[0] << 24) + (array[1] << 16) + (array[2] << 8) + array[3];\n        value -= parseInt(\"ffffffff\", 16);\n    } else { //positive\n        value = (array[0] << 24) + (array[1] << 16) + (array[2] << 8) + array[3];\n    }\n    return value / 10000000;\n}\n\n//**********************************************************************************************\n// parseIOelement\n//**********************************************************************************************\nfunction parseIOelement() {\n    avl.eventIoID = getNextInt16();\n\n    //if (avl.eventIoID == 385) {\n    //    parseBeacon();\n    //    getNextInt8();\n    //} else {\n    parseUsualIOelements();\n    decodeUsualIOelements();\n    //}\n}\n\n//**********************************************************************************************\n// parseBeacon\n//**********************************************************************************************\nfunction parseBeacon(array) {\n    const BEACON_LENGTH = 20;\n    const EDDY_LENGTH = 16;\n\n    var avl = {};\n\n    avl.beaconLength = array.length;\n\n    var ptr = 0;\n\n    // dataPart\n    avl.dataPart = array[ptr++];\n    avl.beaconRecordsCount = avl.dataPart & 0x0F;\n    avl.recordNumber = (avl.dataPart & 0xF0) >> 4;\n\n    if (avl.beaconLength < EDDY_LENGTH) {\n        return avl;\n    }\n\n    // flags\n    avl.flag = array[ptr++];\n    avl.sygnalStrenghAvailable = ((avl.flag & 0x01) > 0) ? 1 : 0;\n    avl.beaconDataSent = ((avl.flag & 0x20) > 0) ? 1 : 0;\n\n    // beacons\n    if (avl.beaconDataSent != 0) {\n        // beacon data\n        avl.beaconId = array.slice(ptr, ptr + BEACON_LENGTH);\n        ptr += BEACON_LENGTH;\n        if (avl.sygnalStrenghAvailable != 0) {\n            avl.BeaconRssi = array[ptr++];\n        }\n    }\n\n    return avl;\n}\n\n//**********************************************************************************************\n// parseUsualIOelements\n//**********************************************************************************************\nfunction parseUsualIOelements() {\n    avl.nTotalIo = getNextInt16();\n    avl.n1Io = getNextInt16();\n    avl.ioData = new Map();\n    var i = 0;\n    for (i = 0; i < avl.n1Io; i++) {\n        var n1IoId = getNextInt16();\n        var n1IoValue = getNextInt8();\n        if (n1IoValue >= 128) {\n            n1IoValue = n1IoValue - 256;\n        }\n        avl.ioData.set(n1IoId, n1IoValue);\n    }\n\n    if (inputDataPtr >= parsedMsg.fullPacketLength) { return [null, null]; }\n    avl.n2Io = getNextInt16();\n    for (i = 0; i < avl.n2Io; i++) {\n        var n2IoId = getNextInt16();\n        var n2IoValue = getNextInt16();\n        if (n2IoValue >= 0x8000) {\n            n2IoValue = n2IoValue - 0x8000;\n        }\n        avl.ioData.set(n2IoId, n2IoValue);\n    }\n\n    if (inputDataPtr >= parsedMsg.fullPacketLength) { return [null, null]; }\n    avl.n4Io = getNextInt16();\n    for (i = 0; i < avl.n4Io; i++) {\n        var n4IoId = getNextInt16();\n        var n4IoValue = getNextInt32();\n        if (n4IoValue >= 0x80000000) {\n            n4IoValue = n4IoValue - 0x80000000;\n        }\n        avl.ioData.set(n4IoId, n4IoValue);\n    }\n\n    if (inputDataPtr >= parsedMsg.fullPacketLength) { return [null, null]; }\n    avl.n8Io = getNextInt16();\n    for (i = 0; i < avl.n8Io; i++) {\n        var n8IoId = getNextInt16();\n        //var n8IoSubArray = getNextSubArray(8);\n        var n8IoValue = getNextInt64();\n        if (n8IoValue >= 0x8000000000000000) {\n            n8IoValue = n8IoValue - 0x8000000000000000;\n        }\n        //node.warn(n8IoValue);\n        avl.ioData.set(n8IoId, n8IoValue);\n    }\n    avl.nxIo = getNextInt16();\n    for (i = 0; i < avl.nxIo; i++) {\n        var nxIoId = getNextInt16();\n        var nxIoLength = getNextInt16();\n        if (nxIoId == 385) {\n            avl.ioData.set(nxIoId, parseBeacon(getNextSubArray(nxIoLength)));\n        } else {\n            avl.ioData.set(nxIoId, getNextSubArray(nxIoLength));\n        }\n    }\n}\n\n//**********************************************************************************************\n// decodeUsualIOelements\n//**********************************************************************************************\nfunction decodeUsualIOelements() {\n    avl.axis = {};\n    avl.axis.x = avl.ioData.get(17);\n    avl.axis.y = avl.ioData.get(18);\n    avl.axis.z = avl.ioData.get(19);\n}\n\n//**********************************************************************************************\n// getCharArray\n//**********************************************************************************************\nfunction getCharArray(array) {\n    const result = [];\n    for (var i = 0; i < array.length; i++) {\n        result.push(String.fromCharCode(array[i]));\n    }\n    return result;\n}\n\n//**********************************************************************************************\n// getNextSubArray\n//**********************************************************************************************\nfunction getNextSubArray(length) {\n    var subarray = inputMsg.slice(inputDataPtr, inputDataPtr + length);\n    inputDataPtr += length;\n    return subarray;\n}\n\n//**********************************************************************************************\n// getNextInt8\n//**********************************************************************************************\nfunction getNextInt8() {\n    return inputMsg[inputDataPtr++];\n}\n\n//**********************************************************************************************\n// getNextInt16\n//**********************************************************************************************\nfunction getNextInt16() {\n    var value = inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    return value;\n}\n\n//**********************************************************************************************\n// getNextInt32\n//**********************************************************************************************\nfunction getNextInt32() {\n    var value = inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    return value;\n}\n\n//**********************************************************************************************\n// getNextInt64\n//**********************************************************************************************\nfunction getNextInt64() {\n    var value = inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    value = (value << 8) + inputMsg[inputDataPtr++];\n    return value;\n}\n\n//**********************************************************************************************\n// Calculates the buffers CRC-16/IBM.\n//**********************************************************************************************\nfunction crc16(buffer, startPtr, length) {\n    var crc = 0;\n    var odd;\n\n    for (var i = 0; i < length; i++) {\n        crc = crc ^ buffer[i + startPtr];\n\n        var numBit = 0;\n        do {\n            odd = crc & 0x0001;\n            crc = crc >> 1;\n            if (odd == 1) {\n                crc = crc ^ 0xA001;\n            }\n            numBit++;\n        } while (numBit < 8);\n    }\n\n    return crc;\n};",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1090,
        "y": 320,
        "wires": [
            [
                "f23873949525accd",
                "70adddf1bbb56e80"
            ],
            [
                "5c4b988d5d63501a"
            ]
        ]
    },
    {
        "id": "f23873949525accd",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "PARSED data",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1180,
        "y": 200,
        "wires": []
    },
    {
        "id": "477efac798c8c72c",
        "type": "inject",
        "z": "1c5b57320a0e5fd6",
        "name": "Trigger",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": "",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 120,
        "y": 80,
        "wires": [
            [
                "f4c9fdc8f4bd3ef0"
            ]
        ]
    },
    {
        "id": "f4c9fdc8f4bd3ef0",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "INIT flow context",
        "func": "const tcpSessions = new Map();\nflow.set(\"TCP_SESSIONS\", tcpSessions);\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 320,
        "y": 80,
        "wires": [
            []
        ]
    },
    {
        "id": "edabd16cc9f5515a",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "SAVE IMEI and send TCP reply",
        "func": "var inputDataPtr = 0;\n\nvar inputMsg = msg.payload;\n\n// IMEI length & IMEI\nvar imeiLength = getNextInt16();\nif (imeiLength != 15) {\n    node.error(\"Wrong imei length: \" + imeiLength);\n    return null;\n}\nvar moduleIMEI = toImeiString(getNextSubArray(imeiLength));\n\nif (msg._session.type !== \"tcp\") {\n    node.warn(\"Wrong session type: \" + msg.session.type);\n    return;\n}\n\nconst TCP_SESSIONS = flow.get(\"TCP_SESSIONS\");\nif (TCP_SESSIONS.get(moduleIMEI)) {\n    node.debug(\"Update session for the IMEI: \" + moduleIMEI);\n    TCP_SESSIONS.set(moduleIMEI, msg._session);\n} else {\n    node.debug(\"Create session for the IMEI: \" + moduleIMEI);\n    TCP_SESSIONS.set(moduleIMEI, msg._session);\n}\nflow.set(\"TCP_SESSIONS\", TCP_SESSIONS);\n\nmsg.payload = Buffer.from(\"\\x01\");\n\nreturn msg;\n\n\n//**********************************************************************************************\n// toImeiString\n//**********************************************************************************************\nfunction toImeiString(IMEIarray) {\n    var result = \"\";\n    for (var i = 0; i < IMEIarray.length; i++) {\n        result += String.fromCharCode(IMEIarray[i]);\n    }\n    return result;\n}\n\n//**********************************************************************************************\n// getNextSubArray\n//**********************************************************************************************\nfunction getNextSubArray(length) {\n    var subarray = inputMsg.slice(inputDataPtr, inputDataPtr + length);\n    inputDataPtr += length;\n    return subarray;\n}\n\n//**********************************************************************************************\n// getNextInt16\n//**********************************************************************************************\nfunction getNextInt16() {\n    var value = inputMsg[inputDataPtr++];\n    value = value * 256 + inputMsg[inputDataPtr++];\n    return value;\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 630,
        "y": 360,
        "wires": [
            [
                "5c4b988d5d63501a"
            ]
        ]
    },
    {
        "id": "5c4b988d5d63501a",
        "type": "tcp out",
        "z": "1c5b57320a0e5fd6",
        "name": "TCP reply",
        "host": "",
        "port": "",
        "beserver": "reply",
        "base64": false,
        "end": false,
        "tls": "",
        "x": 1140,
        "y": 480,
        "wires": []
    },
    {
        "id": "8f8f4e1925c9acfb",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "Filter CONNECT packet",
        "func": "if (msg.payload.length == 17) {\n    return [null, msg];\n}\n\nreturn [msg, null];",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 350,
        "y": 340,
        "wires": [
            [
                "b60f9a85f3b0b907",
                "d1fa3d4f36d49b7e"
            ],
            [
                "708e6312cced8abc",
                "edabd16cc9f5515a"
            ]
        ]
    },
    {
        "id": "708e6312cced8abc",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "CONNECT packet",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 550,
        "y": 420,
        "wires": []
    },
    {
        "id": "b60f9a85f3b0b907",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "Filter existing sessionID",
        "func": "var IMEI = lookForImeiBySession(msg._session);\n\nif (IMEI != null) {\n    return [msg, null];\n}\n\nnode.warn(\"Session with ID: \" + msg._session.id + \" not found\");\nreturn [null, msg];\n\nfunction lookForImeiBySession(session) {\n    var tempIMEI = null;\n    const TCP_SESSIONS = flow.get(\"TCP_SESSIONS\");\n    TCP_SESSIONS.forEach(function (value, key, map) {\n        if(value.id === session.id) {\n            tempIMEI = key;\n        }\n    });\n    return tempIMEI;\n}",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 610,
        "y": 320,
        "wires": [
            [
                "fa702d0bc584f592"
            ],
            [
                "5799fdc1c5dfbc8e"
            ]
        ]
    },
    {
        "id": "5799fdc1c5dfbc8e",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "UNKNOWN session packets",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 820,
        "y": 220,
        "wires": []
    },
    {
        "id": "a8fe1dbcf21ace0d",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "TCP input",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 300,
        "y": 260,
        "wires": []
    },
    {
        "id": "d36be9f4053834d0",
        "type": "tcp in",
        "z": "1c5b57320a0e5fd6",
        "name": "FMC-Input TCP",
        "server": "server",
        "host": "",
        "port": "55555",
        "datamode": "stream",
        "datatype": "buffer",
        "newline": "",
        "topic": "",
        "trim": false,
        "base64": false,
        "tls": "",
        "x": 120,
        "y": 340,
        "wires": [
            [
                "a8fe1dbcf21ace0d",
                "8f8f4e1925c9acfb"
            ]
        ]
    },
    {
        "id": "7c776e6973dba7ea",
        "type": "mqtt out",
        "z": "1c5b57320a0e5fd6",
        "name": "LP2-SoC",
        "topic": "openWB/set/lp/2/%Soc",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "707cfaa6c63d77f1",
        "x": 1720,
        "y": 320,
        "wires": []
    },
    {
        "id": "d1fa3d4f36d49b7e",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "DATA packet",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 530,
        "y": 260,
        "wires": []
    },
    {
        "id": "f0a4c2034771fcc4",
        "type": "comment",
        "z": "1c5b57320a0e5fd6",
        "name": "Src: https://github.com/kaaproject/kaa/blob/master/doc/Tutorials/device-integration/hardware-guides/connect-teltonika-to-kaa-platform/attach/code/node-red-teltonika-tcp-flow.json",
        "info": "",
        "x": 650,
        "y": 580,
        "wires": []
    },
    {
        "id": "70adddf1bbb56e80",
        "type": "function",
        "z": "1c5b57320a0e5fd6",
        "name": "Extract SoC for Polestar 2",
        "func": "if (msg.payload.avl[0].ioData.get(256) != undefined) {   //  VIN enthalten\n    \n    if (msg.payload.avl[0].ioData.get(256).toString() == \"Yxxxxxxxxxxxxxxxx\") {   // VIN des eigenen Fahrzeugs\n    \n        if (msg.payload.avl[0].ioData.get(57) != undefined)  {      // SoC enthalten\n\n            var outMsg = { payload: msg.payload.avl[0].ioData.get(57) + 1 };   // Hybrid battery pack life (= SoC)\n            return outMsg;\n        }\n    }\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1450,
        "y": 320,
        "wires": [
            [
                "9ee2e18f8e060ec7",
                "7c776e6973dba7ea"
            ]
        ]
    },
    {
        "id": "9ee2e18f8e060ec7",
        "type": "debug",
        "z": "1c5b57320a0e5fd6",
        "name": "SoC2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1710,
        "y": 380,
        "wires": []
    },
    {
        "id": "707cfaa6c63d77f1",
        "type": "mqtt-broker",
        "name": "openWB",
        "broker": "192.168.0.41",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]

Nur der banale rechte Teil ab „Extract SoC …“ ist wirklich von mir. Dabei war lediglich zu beachten, dass beim PS der SoC im Feld „Hybrid battery pack life“ steckt, was eigentlich irreführend ist. Bitte die eigene VIN im Quelltext noch eintragen, sonst kommt nie etwas an.

Vom Rest des Flows ist die Quelle bereits oben genannt und auch im Export enthalten.

Damit etwas empfangen wird, müssen folgende Voraussetzungen erfüllt sein:

  • SIM-Karte mit etwas Datentraffic muss im FMC stecken (ich habe eine Netzclub-Karte mit monatlich kostenlosen 200MB Traffic, gibt es leider seit heute nicht mehr neu. Mal schauen, wann ich mich nach einer anderen Lösung umschauen muss. Sämtlich IoT-SIMs, die ich gefunden habe, sind signifikant teurer als die billigsten Varianten der diversen Discounter).

  • SIM-Karte muss PIN-befreit sein (Lt. FMC-Anleitung kann man die PIN auch im Gerät hinterlegen, hat bei mir aber nicht zuverlässig funktioniert)

  • Irgendein Server muss im Netz erreichbar sein und den Traffic an den Input-Port des Flows weiterreichen. Dessen Adresse und Port muss via Teltonika Configurator und USB bzw. Bluetooth in den FMC Settings unter GPRS / Server Settings (sowie Protocol „TCP“) eingetragen werden. Eine TLS-verschlüsselte Verbindung habe ich noch nicht hinbekommen, würde auch hier konfiguriert

  • Unter OBDII sollten „OBD (Auto)“, VIN Source „Manual“ und die VIN selbst eingetragen werden. Weiter unten wird eingestellt, welche Daten im Paket geliefert werden sollen. Hier ist mindestens „Hybrid Battery Pack Remaining Life“ auf Low zu setzen.

  • FMC muss im Fahrzeug am OBD-Stecker angesteckt sein

Zum Schluss muss ich mir noch selbst widersprechen und zurückrudern. Der SoH ist beim Polestar tatsächlich nicht verfügbar. Den habe ich nur bei meinem Peugeot e208 gesehen (98%). Peugeot verwendet aber auch die m.E. richtigen Felder, nämlich „OEM Battery Charge Level“ und „OEM Battery State Of Health“. Bei dem seltsamen Datenfeld „Hybrid …“ gibt es nichts in der Nähe, was nach SoH aussieht. Also sorry für die unnötige Vorfreude, aber vielleicht ist das ganze auch so hilfreich.

Gruß
Ralph

1 „Gefällt mir“