API zum Auslesen der Lade- / Verbrauchsdaten

So einfach ist das nicht. Du musst dich erst authentifizieren und dir einen AuthToken holen, bevor du dir per GraphQl query tatsächlich Daten holen kannst.

Die Diskussion könnte ein guter Start für dich sein: Polestar API Down: New API Endpoint "/mystar-v2" · Issue #96 · leeyuentuen/polestar_api · GitHub

Hat das Thema jemand schon in NodeRed gelöst? ich bin zu dumm dafür und habe es leider noch nicht geschafft.

So, hab es nun selbst zusammen gebracht - Für alle die auch gerne NodeRed verwenden wollen:

[{"id":"fd45e125eed8839a","type":"function","z":"8f1e61194be045a8","name":"getAllCarData","func":"// Code added here will be run once\n// whenever the node is started.\n\nclass Polestar {\n    #credentials = {\n        email: null,\n        password: null,\n    }\n\n    #token = {\n        access: null,\n        refresh: null,\n        expires: null,\n    }\n\n    #vehicle = {\n        vin: null,\n        id: null,\n    }\n\n    constructor(email, password) {\n        if (!email || !password) {\n            throw new Error(\"Email and password must be provided\")\n        }\n        this.#credentials.email = email\n        this.#credentials.password = password\n    }\n\n    async login() {\n        const { pathToken, cookie } = await this.#getLoginFlowTokens()\n        const tokenRequestCode = await this.#performLogin(pathToken, cookie)\n        const apiCreds = await this.#getApiToken(tokenRequestCode)\n        await this.#storeToken(apiCreds)\n        if (!await this.#checkAuthenticated()) {\n            throw new Error(\"Login failed, token is not valid\")\n        }\n    }\n\n    async #storeToken(token) {\n        const apiCreds = token\n\n        const tokenExpiryTime = new Date()\n        const secondsToAdd = apiCreds.expires_in\n        tokenExpiryTime.setSeconds(tokenExpiryTime.getSeconds() + (secondsToAdd - 120))\n\n        this.#token.access = apiCreds.access_token\n        this.#token.refresh = apiCreds.refresh_token\n        this.#token.expires = tokenExpiryTime\n    }\n\n    async #checkAuthenticated() {\n        if (!this.#token.access || !this.#token.refresh || !this.#token.expires) {\n            throw new Error(\"Not logged in\")\n        }\n        if (this.#token.expires < new Date()) {\n            const apiCreds = await this.#refreshToken()\n            this.#storeToken(apiCreds)\n        }\n\n        if (await this.#checkTokenValidity()) {\n            return true\n        } else {\n            throw new Error(\"Token is not valid or refresh token has expired\")\n        }\n\n    }\n\n    async #refreshToken() {\n        console.log(\"Refreshing token\")\n        const response = await axios.post(\n            \"https://pc-api.polestar.com/eu-north-1/auth\",\n            \"{\\\"query\\\":\\\"\\\\n  query refreshAuthToken($token: String!) {\\\\n    refreshAuthToken(token: $token) {\\\\n      access_token\\\\n      expires_in\\\\n      id_token\\\\n      refresh_token\\\\n    }\\\\n  }\\\\n\\\",\\\"variables\\\":{\\\"token\\\":\\\"\" + this.#token.refresh + \"\\\"}}\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    \"Authorization\": \"Bearer \" + this.#token.access,\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n                validateStatus: function (status) {\n                    return true\n                },\n            }\n        )\n        const data = await response.data\n        const apiCreds = data.data.refreshAuthToken\n        return {\n            access_token: apiCreds.access_token,\n            refresh_token: apiCreds.refresh_token,\n            expires_in: apiCreds.expires_in,\n        }\n    }\n\n    async #checkTokenValidity() {\n        const response = await axios.get(\n            \"https://pc-api.polestar.com/eu-north-1/my-star/?query=query%20introspectToken(%24token%3A%20String!)%20%7B%0A%20%20introspectToken(token%3A%20%24token)%20%7B%0A%20%20%20%20active%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D&operationName=introspectToken&variables=%7B%22token%22%3A%22\" + this.#token.access + \"%22%7D\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    \"Authorization\": \"Bearer \" + this.#token.access,\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n                validateStatus: function (status) {\n                    return true\n                },\n            }\n        )\n        const data = await response.data.data\n        if (!data.introspectToken || !data.introspectToken.active) {\n            return false\n        } else {\n            return true\n        }\n    }\n\n    async #performLogin(pathToken, cookie) {\n        const response = await axios.post(\n            \"https://polestarid.eu.polestar.com/as/\" + pathToken + \"/resume/as/authorization.ping?client_id=polmystar\",\n            {\n                \"pf.username\": this.#credentials.email,\n                \"pf.pass\": this.#credentials.password,\n            },\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/x-www-form-urlencoded\",\n                    pragma: \"no-cache\",\n                    cookie: cookie,\n                },\n                maxRedirects: 0,\n                validateStatus: function (status) {\n                    return true\n                },\n            }\n        )\n\n        const redirectUrl = response.headers.location\n        const regex = /code=([^&]+)/\n        const match = redirectUrl.match(regex)\n        const tokenRequestCode = match ? match[1] : null\n\n        return tokenRequestCode\n    }\n\n    async #getLoginFlowTokens() {\n        const response = await axios.get(\n            \"https://polestarid.eu.polestar.com/as/authorization.oauth2?response_type=code&client_id=polmystar&redirect_uri=https%3A%2F%2Fwww.polestar.com%2Fsign-in-callback&scope=openid%20profile%20email%20customer:attributes%20customer:attributes:write\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    pragma: \"no-cache\",\n                },\n                referrerPolicy: \"strict-origin-when-cross-origin\",\n                body: null,\n                maxRedirects: 0,\n                method: \"GET\",\n                validateStatus: function (status) {\n                    return true\n                },\n            }\n        )\n        const data = await response\n        const redirectUrl = response.headers.location\n        const regex = /resumePath=(\\w+)/\n        const match = redirectUrl.match(regex)\n        const pathToken = match ? match[1] : null\n        const cookies = response.headers[\"set-cookie\"]\n        const cookie = cookies[0].split('; ')[0] + \";\"\n        return {\n            pathToken: pathToken,\n            cookie: cookie\n        }\n    }\n\n    async #getApiToken(tokenRequestCode) {\n        const response = await axios.get(\n            \"https://pc-api.polestar.com/eu-north-1/auth/?query=query%20getAuthToken(%24code%3A%20String!)%20%7B%0A%20%20getAuthToken(code%3A%20%24code)%20%7B%0A%20%20%20%20id_token%0A%20%20%20%20access_token%0A%20%20%20%20refresh_token%0A%20%20%20%20expires_in%0A%20%20%7D%0A%7D%0A&operationName=getAuthToken&variables=%7B%22code%22%3A%22\" + tokenRequestCode + \"%22%7D\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n                validateStatus: function (status) {\n                    return true\n                },\n            }\n        )\n        const data = await response.data\n        const apiCreds = data.data.getAuthToken\n        return {\n            access_token: apiCreds.access_token,\n            refresh_token: apiCreds.refresh_token,\n            expires_in: apiCreds.expires_in,\n        }\n    }\n\n    async getVehicles() {\n        if (!await this.#checkAuthenticated()) {\n            throw new Error(\"Not authenticated\")\n        }\n        const response = await axios.get(\n            \"https://pc-api.polestar.com/eu-north-1/my-star/?query=query%20getCars%20%7B%0A%20%20getConsumerCarsV2%20%7B%0A%20%20%20%20vin%0A%20%20%20%20internalVehicleIdentifier%0A%20%20%20%20modelYear%0A%20%20%20%20content%20%7B%0A%20%20%20%20%20%20model%20%7B%0A%20%20%20%20%20%20%20%20code%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20__typename%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20images%20%7B%0A%20%20%20%20%20%20%20%20studio%20%7B%0A%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20angles%0A%20%20%20%20%20%20%20%20%20%20__typename%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20__typename%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20__typename%0A%20%20%20%20%7D%0A%20%20%20%20hasPerformancePackage%0A%20%20%20%20registrationNo%0A%20%20%20%20deliveryDate%0A%20%20%20%20currentPlannedDeliveryDate%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D&operationName=getCars&variables=%7B%7D\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    \"Authorization\": \"Bearer \" + this.#token.access,\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n            }\n        )\n        if (!response.data.data.getConsumerCarsV2) {\n            throw new Error(\"No vehicles found\")\n        }\n        const vehicles = response.data.data.getConsumerCarsV2\n        return vehicles\n    }\n\n    async setVehicle(vin) {\n        if (!await this.#checkAuthenticated()) {\n            throw new Error(\"Not authenticated\")\n        }\n        const vehicles = await this.getVehicles()\n        let vehicle = null\n        if (vin) {\n            vehicle = vehicles.find((vehicle) => vehicle.vin === vin)\n        } else {\n            vehicle = vehicles[0]\n        }\n        if (!vehicle) {\n            throw new Error(\"Vehicle not found\")\n        }\n        this.#vehicle.vin = vehicle.vin\n        this.#vehicle.id = vehicle.internalVehicleIdentifier\n\n        return this.#vehicle\n    }\n\n    async getBattery() {\n        if (!await this.#checkAuthenticated()) {\n            throw new Error(\"Not authenticated\")\n        }\n\n        if (!this.#vehicle.vin) {\n            throw new Error(\"No vehicle selected\")\n        }\n\n        const response = await axios.get(\n            \"https://pc-api.polestar.com/eu-north-1/mystar-v2?query=query%20GetBatteryData(%24vin%3A%20String!)%20%7B%0A%20%20getBatteryData(vin%3A%20%24vin)%20%7B%0A%20%20%20%20averageEnergyConsumptionKwhPer100Km%0A%20%20%20%20batteryChargeLevelPercentage%0A%20%20%20%20chargerConnectionStatus%0A%20%20%20%20chargingCurrentAmps%0A%20%20%20%20chargingPowerWatts%0A%20%20%20%20chargingStatus%0A%20%20%20%20estimatedChargingTimeMinutesToTargetDistance%0A%20%20%20%20estimatedChargingTimeToFullMinutes%0A%20%20%20%20estimatedDistanceToEmptyKm%0A%20%20%20%20estimatedDistanceToEmptyMiles%0A%20%20%20%20eventUpdatedTimestamp%20%7B%0A%20%20%20%20%20%20iso%0A%20%20%20%20%20%20unix%0A%20%20%20%20%20%20__typename%0A%20%20%20%20%7D%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D&operationName=GetBatteryData&variables=%7B%22vin%22%3A%22\" + this.#vehicle.vin + \"%22%7D\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    \"Authorization\": \"Bearer \" + this.#token.access,\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n            }\n        )\n        const data = await response.data.data.getBatteryData\n        return data\n    }\n\n    async getOdometer() {\n        if (!await this.#checkAuthenticated()) {\n            throw new Error(\"Not authenticated\")\n        }\n\n        if (!this.#vehicle.vin) {\n            throw new Error(\"No vehicle selected\")\n        }\n\n        const response = await axios.get(\n            \"https://pc-api.polestar.com/eu-north-1/mystar-v2?query=query%20GetOdometerData(%24vin%3A%20String!)%20%7B%0A%20%20getOdometerData(vin%3A%20%24vin)%20%7B%0A%20%20%20%20averageSpeedKmPerHour%0A%20%20%20%20eventUpdatedTimestamp%20%7B%0A%20%20%20%20%20%20iso%0A%20%20%20%20%20%20unix%0A%20%20%20%20%20%20__typename%0A%20%20%20%20%7D%0A%20%20%20%20odometerMeters%0A%20%20%20%20tripMeterAutomaticKm%0A%20%20%20%20tripMeterManualKm%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D&operationName=GetOdometerData&variables=%7B%22vin%22%3A%22\" + this.#vehicle.vin + \"%22%7D\",\n            {\n                headers: {\n                    \"cache-control\": \"no-cache\",\n                    \"content-type\": \"application/json\",\n                    \"Authorization\": \"Bearer \" + this.#token.access,\n                    pragma: \"no-cache\",\n                },\n                maxRedirects: 0,\n            }\n        )\n        const data = await response.data.data.getOdometerData\n        return data\n    }\n}\n\n\n\nconst polestar = new Polestar(\"DEINE EMAIL\", \"DEIN PASSWORD\");\nawait polestar.login();\nconst vehicles = await polestar.getVehicles();\nawait polestar.setVehicle(vehicles[0].vin);\nconst batt = await polestar.getBattery();\nconst odo = await polestar.getOdometer();\n\nmsg.odo = odo;\nmsg.vehicles = vehicles;\nmsg.bat = batt;\nmsg.payload = vehicles[0].content.model.name;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"axios","module":"axios"}],"x":700,"y":200,"wires":[["cd43d91816497c1e","5667b87be88d9f5d","34467ccf11dbacf2","a0aec7100a98d524","d5f544c8916f6ffd","9737340fef439758","377e31a58cc94d81","d59067120006bbd4"]]},{"id":"bf870353702a3547","type":"inject","z":"8f1e61194be045a8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":440,"y":200,"wires":[["fd45e125eed8839a"]]},{"id":"cd43d91816497c1e","type":"debug","z":"8f1e61194be045a8","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":980,"y":120,"wires":[]},{"id":"71042cdc04822d04","type":"debug","z":"8f1e61194be045a8","name":"Soc","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1350,"y":200,"wires":[]},{"id":"5667b87be88d9f5d","type":"function","z":"8f1e61194be045a8","name":"SoC","func":"msg.payload = msg.bat.batteryChargeLevelPercentage;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":200,"wires":[["71042cdc04822d04"]]},{"id":"a0aec7100a98d524","type":"function","z":"8f1e61194be045a8","name":"Verbrauch/100km","func":"msg.payload = msg.bat.averageEnergyConsumptionKwhPer100Km\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":300,"wires":[["00ba973a886207f7"]]},{"id":"d5f544c8916f6ffd","type":"function","z":"8f1e61194be045a8","name":"Steckerstatus","func":"msg.payload = msg.bat.chargerConnectionStatus\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":350,"wires":[["d4237b28ebf84db4"]]},{"id":"34467ccf11dbacf2","type":"function","z":"8f1e61194be045a8","name":"Distance","func":"msg.payload = msg.bat.estimatedDistanceToEmptyKm\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":960,"y":250,"wires":[["2ca76bfddd6b8b9e"]]},{"id":"9737340fef439758","type":"function","z":"8f1e61194be045a8","name":"Restladezeit","func":"msg.payload = msg.bat.estimatedChargingTimeToFullMinutes;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":970,"y":400,"wires":[["97cfe32175d2262d"]]},{"id":"377e31a58cc94d81","type":"function","z":"8f1e61194be045a8","name":"Kilometerstand","func":"msg.payload = msg.odo.odometerMeters / 1000;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":450,"wires":[["4fc69f5975c3c82c"]]},{"id":"d59067120006bbd4","type":"function","z":"8f1e61194be045a8","name":"Durchschnitsgeschwindigkeit","func":"msg.payload = msg.odo.averageSpeedKmPerHour;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1020,"y":500,"wires":[["fa41b75b538a01b1"]]},{"id":"2ca76bfddd6b8b9e","type":"debug","z":"8f1e61194be045a8","name":"Reichweite","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1370,"y":250,"wires":[]},{"id":"00ba973a886207f7","type":"debug","z":"8f1e61194be045a8","name":"Durchschnitsverbrauch","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1400,"y":300,"wires":[]},{"id":"d4237b28ebf84db4","type":"debug","z":"8f1e61194be045a8","name":"Steckerstatus","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1380,"y":350,"wires":[]},{"id":"4fc69f5975c3c82c","type":"debug","z":"8f1e61194be045a8","name":"Kilometerstand","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1380,"y":450,"wires":[]},{"id":"fa41b75b538a01b1","type":"debug","z":"8f1e61194be045a8","name":"Durchschnitsgeschwindigkeit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":500,"wires":[]},{"id":"97cfe32175d2262d","type":"debug","z":"8f1e61194be045a8","name":"Restladezeit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1370,"y":400,"wires":[]}]
1 „Gefällt mir“

Hey @bausi2k
Das funktioniert perfekt !!!
Vielen herzlichen Dank für das Publizieren :+1:t2::blush:
Andi

1 „Gefällt mir“

ich bekomme aktuell einen Fehler (seit heute Nacht) / irgendwas mit dem Passwort, hat das Thema noch jemand?

Ja, Polestar hat wieder was geändert :wink:

Hier gibt es schon einen Fix, es hat sich wohl unter anderem die Client ID geändert:

1 „Gefällt mir“

jetzt muss nur wer noch die JS API updaten… oder ich muss mir eine HA Instanz hochziehen :frowning:

Ist es wirklich nur die client_ID, die sich geändert hat und dass man sich auf polestar.com einloggen und die neuen Terms & Conditions durchwinken muss?

Also so einfach ist es wohl nicht.
Man verbessere mich, wenn ich das nicht richtig kapiert habe:
Es reicht NICHT sich manuell auf polestar.com einzuloggen und die terms & conditions abzunicken.
Das gleiche muss man anscheinend über die API realisieren. Möglicherweise nur einmal.
Wie das auszusehen hat, muss ich erstmal verstehen.

Korrekt @Sipple ! Es sind mehrere Änderungen, die Umgesetzt werden müssen. Unter anderem die neue Client ID und das Akzeptieren der T&Cs.

Über die Tibber API funktioniert es weiterhin …

Interessant. Soweit ich mich erinnere ist die Tibber Integration aber auch offiziell. Ich würde daher davon ausgehen, dass sie vorab informiert wurden und entsprechend reagieren konnten.

Kann ich verneinen. Tibber API ist bei mir zeitgleich ausgestiegen.

Stand 14:01 geht es bei mir noch einwandfrei über die Tibber API …

Nun, die API Abfrage ansich geht, aber in meinem Fall ist der Polestar schlicht nicht mehr vorhanden.

{‚data‘: {‚me‘: {‚homes‘: [{‚electricVehicles‘: [{‚shortName‘: ‚e-up!‘, ‚lastSeen‘: ‚2024-11-07T14:34:03+01:00‘, ‚lastSeenText‘: ‚Last updated: a few seconds ago‘, ‚battery‘: {‚percent‘: 49, ‚isCharging‘: False}}]}]}}}

Nur noch der e-up is zu sehen. Das passierte wie gesagt ziemlich zeitgleich zum Ausstieg der Polestar API. Wäre schon ein großer Zufall, wenn das nichts miteinander zu tun hätte.

Edit:
Tibber hat den Polestar wie gesagt fast zeitgleich zur API Änderung einfach mal komplett aus meinem Account gelöscht.
Jetzt ist er wieder drin, aber Tibber definiert nun den Polestar mit dem Index 0 und den e-up mit 1. Vorher war das genau umgekehrt. Super Sache!

In Home Assistant gibt es für die Polestar API Integration mittlerweile ein Update auf 1.8.0. Im Changelog heißt es: „General OIDC improvements (including support for updated polestar ID login).“

Vor dem Update hatte Home Assistant lediglich keine Verbindung mehr.
Nach dem Update kommen nun aber Fehlermeldungen:

"Setup failed for custom integration polestar_api: Unable to import component: Exception importing custom_components.polestar_api

09:29:17 – (FEHLER) setup.py

Unexpected exception importing component custom_components.polestar_api

09:29:17 – (FEHLER) loader.py"

Hat das noch jemand?

Nein, diese Fehler hatte ich nicht. Allerdings wurde ein neues Gerät angelegt.
Ich habe dann die Integration Polestar-Api entfernt und noch mal neu hinzugefügt.
Dann die Zugangsdaten eingeben und es wurde nur noch ein Gerät gefunden.

So, bei mir geht es auch wieder. Ich hatte Home Assistant Core, OS und HACS noch auf älteren Versionsständen. Irgendwo hat es da gehakt.
Allerdings wird bei mir das Fahrzeugbild jetzt nicht mehr angezeigt (Entität: image.polestar_car_image).

Die Entität musst du aktivieren. Die ist aus irgendwelchen Gründen deaktiviert.
Danach geht es dann auch wieder.

1 „Gefällt mir“

Hat geklappt! Du bist ab sofort mein Home Assistant Experte!