diff --git a/docs/SoC-Skoda-settings-1.PNG b/docs/SoC-Skoda-settings-1.PNG new file mode 100644 index 0000000000..892f4d24e6 Binary files /dev/null and b/docs/SoC-Skoda-settings-1.PNG differ diff --git a/docs/SoC-Skoda-settings-2.PNG b/docs/SoC-Skoda-settings-2.PNG new file mode 100644 index 0000000000..6805cea332 Binary files /dev/null and b/docs/SoC-Skoda-settings-2.PNG differ diff --git a/docs/SoC-Skoda.md b/docs/SoC-Skoda.md new file mode 100644 index 0000000000..a994abf327 --- /dev/null +++ b/docs/SoC-Skoda.md @@ -0,0 +1,82 @@ +# SoC-Modul Skoda + +Das SoC-Modul Skoda gibt es in openWB 2.x. + +## Konfiguration + +Die Konfiguration erfolgt im Bereich Einstellungen - Konfiguration - Fahrzeuge: + +![Allgemeine Konfiguration](SoC-Skoda-settings-1.PNG) + +![Spezielle Konfiguration](SoC-Skoda-settings-2.PNG) + +## Hinweise + +Für nicht-Skoda Fahrzeuge (Audi, VW, etc.) funktioniert das Modul nicht. + +Erfolgreich getestet u.a. für folgende Fahrzeuge: Enyaq. + +**Wichtig für alle Fahrzeuge:** +Es muss ein aktives Konto im Skoda ID Portal vorhanden sein und die "MySkoda App" muss eingerichtet sein. + +**WICHTIG:** +Skoda ändert gelegentlich die Bedingungen für die Nutzung der Online-Services. + +Diese müssen bestätigt werden. Wenn der SOC-Abruf plötzlich nicht mehr funktioniert, VOR dem Posten bitte Schritt 1 ausführen. + +Bei Problemen zunächst bitte diese Schritte durchführen: + +1. sicherstellen, dass auf dieser Skoda-Seite alle Einverständnisse gegeben wurden. + + + + In einigen Fällen wurden die Einverständnisse gegeben und trotzdem funktionierte die Abfrage nicht. + Hier hat folgendes Vorgehen geholfen: Im Skoda Konto das Land temporär umstellen, d.h. + - auf ein anderes Land als das eigene ändern + - sichern + - zurück auf das eigene Land ändern + - sichern. + +2. Nach einem manuellen SOC-Abruf (Kreispfeil hinter dem SOC klicken) auf der Status - Seite EV-SOC Log und Debug log auf Fehler kontrollieren + +3. Falls im Ev-Soc Log Fehler 303 (unknown redirect) gemeldet wird: + - Ursache 1: Bestimmte Sonderzeichen im Passwort funktionieren nicht mit dem Modul. Bitte das Passwort auf eines ohne Sonderzeichen ändern und testen. + - Ursache 2: Falsche Email, Passwort oder VIN eingegeben. Alle 3 löschen, speichern, neu eingeben, speichern und testen. + +4. Falls eine Firewall im Spiel ist: Es gab einzelne Probleme beim Internet-Zugriff der openWB auf Python Archive und Fahrzeug-Server wenn IPV6 aktiv ist. + +5. Nach Neustart bzw. Änderung der LP-Konfiguration werden im EV-Soc-Log Fehler ausgegeben (permission oder fehlende Datei). + + Diese Fehler sind normal und können ignoriert werden. Leider wird im Debug Mode 0 keine Positiv-Meldung ausgegeben. + Empfehlung: + - In Einstellungen - System - Fehlersuche dies einstellen: Debug Level/Details + - dann einen manuellen SOC-Abruf durchführen (im Dashboard auf Kreispfeil klicken). + - danach sollte im EV-SOC-Log eine Zeile ähnlich dieser kommen: + + `2023-02-12 11:57:14 INFO:soc_skoda:Lp1 SOC: 61%@2023-02-12T11:53:20` + + Diese Zeile zeigt folgende Information: + + `2023-02-12 11:57:14` *- Timestamp des SOC-Abrufs* + + `INFO` *- Debug Level INFO* + + `soc_skoda` *- SOC-Modul* + + `Lp1` *- Ladepunkt* + + `SOC: 61%` *- SOC Stand* + + `@2025-02-12T11:53:20` *- Timestamp des Updates vom EV zum VW Cloud-Server* + +6. Falls diese Schritte nicht zum Erfolg führen, das Problem im [Support Thema](https://forum.openwb.de/viewforum.php?f=12) mit Angabe relevanter Daten posten + - oWB SW Version + - oWB gekauft oder selbst installiert + - wenn selbst installiert: welches OS(Stretch/Buster) + - welches Fahrzeug + - falls vorhanden Angaben über Firewall, VPN, etc., also Appliances, die den Internetzugang limitieren könnten + - relevante Abschnitte der Logs, vor allem Fehlermeldungen, als CODE-blocks (). + +Das SoC-Log mit evtl. Fehlermeldungen kann wie folgt eingesehen werden: + +- Einstellungen - System - Fehlersuche diff --git a/packages/modules/vehicles/skoda/__init__.py b/packages/modules/vehicles/skoda/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/packages/modules/vehicles/skoda/api.py b/packages/modules/vehicles/skoda/api.py new file mode 100755 index 0000000000..33cfb805b8 --- /dev/null +++ b/packages/modules/vehicles/skoda/api.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import aiohttp +from asyncio import new_event_loop, set_event_loop +from typing import Union +from modules.vehicles.skoda import libskoda +from modules.vehicles.skoda.config import Skoda +from modules.vehicles.vwgroup.vwgroup import VwGroup + + +class api(VwGroup): + + def __init__(self, conf: Skoda, vehicle: int): + super().__init__(conf, vehicle) + + # async method, called from sync fetch_soc, required because libvwid/libskoda expect async environment + async def _fetch_soc(self) -> Union[int, float, str]: + async with aiohttp.ClientSession() as self.session: + skoda = libskoda.skoda(self.session) + return await super().request_data(skoda) + + +def fetch_soc(conf: Skoda, vehicle: int) -> Union[int, float, str]: + + # prepare and call async method + loop = new_event_loop() + set_event_loop(loop) + + # get soc, range from server + a = api(conf, vehicle) + soc, range, soc_ts, soc_tsX = loop.run_until_complete(a._fetch_soc()) + + return soc, range, soc_ts, soc_tsX diff --git a/packages/modules/vehicles/skoda/config.py b/packages/modules/vehicles/skoda/config.py new file mode 100755 index 0000000000..d91a690596 --- /dev/null +++ b/packages/modules/vehicles/skoda/config.py @@ -0,0 +1,28 @@ +from typing import Optional + + +class SkodaConfiguration: + def __init__(self, + user_id: Optional[str] = None, # show in UI + password: Optional[str] = None, # show in UI + vin: Optional[str] = None, # show in UI + refreshToken: Optional[str] = None, # DON'T show in UI! + calculate_soc: bool = False # show in UI + ): + self.user_id = user_id + self.password = password + self.vin = vin + self.refreshToken = refreshToken + self.calculate_soc = calculate_soc + + +class Skoda: + def __init__(self, + name: str = "Skoda", + type: str = "skoda", + official: bool = False, + configuration: SkodaConfiguration = None) -> None: + self.name = name + self.type = type + self.official = official + self.configuration = configuration or SkodaConfiguration() diff --git a/packages/modules/vehicles/skoda/libskoda.py b/packages/modules/vehicles/skoda/libskoda.py new file mode 100755 index 0000000000..89b7b3a284 --- /dev/null +++ b/packages/modules/vehicles/skoda/libskoda.py @@ -0,0 +1,254 @@ +# A Python class to communicate with the "Skoda Connect" API. +# Adapted the libvwid.py module to skoda interface + +import secrets +import logging +import json +import uuid +import base64 +import hashlib + +from helpermodules.utils.error_handling import ImportErrorContext +with ImportErrorContext(): + import lxml.html + +# Constants +LOGIN_BASE = "https://identity.vwgroup.io/oidc/v1" +LOGIN_HANDLER_BASE = "https://identity.vwgroup.io" +API_BASE = "https://mysmob.api.connect.skoda-auto.cz/api" +CLIENT_ID = "7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com" + + +class skoda: + def __init__(self, session): + self.session = session + self.headers = {} + self.log = logging.getLogger(__name__) + self.jobs_string = 'all' + + def form_from_response(self, text): + page = lxml.html.fromstring(text) + elements = page.xpath('//form//input[@type="hidden"]') + form = {x.attrib['name']: x.attrib['value'] for x in elements} + return (form, page.forms[0].action) + + def password_form(self, text): + page = lxml.html.fromstring(text) + elements = page.xpath('//script') + + # Todo: Find more elegant way parse this... + objects = {} + for a in elements: + if (a.text) and (a.text.find('window._IDK') != -1): + text = a.text.strip() + text = text[text.find('\n'):text.rfind('\n')].strip() + for line in text.split('\n'): + try: + (name, val) = line.strip().split(':', 1) + except ValueError: + continue + val = val.strip('\', ') + objects[name] = val + + json_model = json.loads(objects['templateModel']) + + if ('errorCode' in json_model): + self.log.error("Login error: %s", json_model['errorCode']) + return False + + try: + # Generate form + form = {} + form['relayState'] = json_model['relayState'] + form['hmac'] = json_model['hmac'] + form['email'] = json_model['emailPasswordForm']['email'] + form['_csrf'] = objects['csrf_token'] + + # Generate URL action + action = '/signin-service/v1/%s/%s'\ + % (json_model['clientLegalEntityModel']['clientId'], json_model['postAction']) + + return (form, action) + + except KeyError: + self.log.exception("Missing fields in response from Skoda API") + return False + + def set_vin(self, vin): + self.vin = vin + + def set_credentials(self, username, password): + self.username = username + self.password = password + + def set_jobs(self, jobs): + self.jobs_string = ','.join(jobs) + + def get_code_challenge(self): + code_verifier = secrets.token_urlsafe(64).replace('+', '-').replace('/', '_').replace('=', '') + code_challenge = base64.b64encode(hashlib.sha256(code_verifier.encode('utf-8')).digest()) + code_challenge = code_challenge.decode('utf-8').replace('+', '-').replace('/', '_').replace('=', '') + return (code_verifier, code_challenge) + + async def connect(self, username, password): + self.set_credentials(username, password) + return (await self.reconnect()) + + async def reconnect(self): + # Get code challenge and verifier + code_verifier, code_challenge = self.get_code_challenge() + + # Get authorize page + _scope = 'address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier' + _scope = _scope + ' openid phone profession profile vin' + payload = { + 'client_id': CLIENT_ID, + 'scope': _scope, + 'response_type': 'code id_token', + 'nonce': secrets.token_urlsafe(12), + 'redirect_uri': 'myskoda://redirect/login/', + 'state': str(uuid.uuid4()), + 'code_challenge': code_challenge, + 'code_challenge_method': 'S256' + } + + response = await self.session.get(LOGIN_BASE + '/authorize', params=payload) + if response.status >= 400: + self.log.error(f"Authorize: Non-2xx response ({response.status})") + # Non 2xx response, failed + return False + + # Fill form with email (username) + (form, action) = self.form_from_response(await response.read()) + form['email'] = self.username + response = await self.session.post(LOGIN_HANDLER_BASE+action, data=form) + if response.status >= 400: + self.log.error("Email: Non-2xx response") + return False + + # Fill form with password + (form, action) = self.password_form(await response.read()) + form['password'] = self.password + url = LOGIN_HANDLER_BASE + action + response = await self.session.post(url, data=form, allow_redirects=False) + + # Can get a 303 redirect for a "terms and conditions" page + if (response.status == 303): + url = response.headers['Location'] + if ("terms-and-conditions" in url): + # Get terms and conditions page + url = LOGIN_HANDLER_BASE + url + response = await self.session.get(url, data=form, allow_redirects=False) + (form, action) = self.form_from_response(await response.read()) + + url = LOGIN_HANDLER_BASE + action + response = await self.session.post(url, data=form, allow_redirects=False) + + self.log.warn("Agreed to terms and conditions") + else: + self.log.error("Got unknown 303 redirect") + return False + + # Handle every single redirect and stop if the redirect + # URL uses the weconnect adapter. + while (True): + url = response.headers['Location'] + if (url.split(':')[0] == "myskoda"): + if not ('id_token' in url): + self.log.error("Missing id token") + return False + # Parse query string + query_string = url.split('#')[1] + query = {x[0]: x[1] for x in [x.split("=") for x in query_string.split("&")]} + break + + if (response.status != 302): + self.log.error("Not redirected, status %u" % response.status) + return False + + response = await self.session.get(url, data=form, allow_redirects=False) + + self.headers = dict(response.headers) + + # Get final token + params = { + 'tokenType': 'CONNECT' + } + payload = { + 'code': query['code'], + 'redirectUri': "myskoda://redirect/login/", + 'verifier': code_verifier + } + response = await self.session.post(API_BASE + '/v1/authentication/exchange-authorization-code', + params=params, json=payload) + if response.status >= 400: + self.log.error("Login: Non-2xx response") + # Non 2xx response, failed + return False + self.tokens = await response.json() + + # Update header with final token + self.headers['Authorization'] = 'Bearer %s' % self.tokens["accessToken"] + + # Success + return True + + async def refresh_tokens(self): + if not self.headers: + return False + + params = { + 'tokenType': 'CONNECT' + } + # Use the refresh token + payload = { + 'token': self.tokens["refreshToken"] + } + + response = await self.session.post(API_BASE + '/v1/authentication/refresh-token', params=params, json=payload) + if response.status >= 400: + return False + self.tokens = await response.json() + + # Use the newly received access token + self.headers['Authorization'] = 'Bearer %s' % self.tokens["accessToken"] + + return True + + async def get_status(self): + status_url = f"{API_BASE}/v2/vehicle-status/{self.vin}/driving-range" + response = await self.session.get(status_url, headers=self.headers) + + # If first attempt fails, try to refresh tokens + if response.status >= 400: + self.log.debug("Refreshing tokens") + if await self.refresh_tokens(): + response = await self.session.get(status_url, headers=self.headers) + + # If refreshing tokens failed, try a full reconnect + if response.status >= 400: + self.log.info("Reconnecting") + if await self.reconnect(): + response = await self.session.get(status_url, headers=self.headers) + else: + self.log.error("Reconnect failed") + return {} + + if response.status >= 400: + self.log.error("Get status failed") + return {} + + status_data = await response.json() + self.log.debug(f"Status data from Skoda API: {status_data}") + + return { + 'charging': { + 'batteryStatus': { + 'value': { + 'currentSOC_pct': status_data['primaryEngineRange']['currentSoCInPercent'], + 'cruisingRangeElectric_km': status_data['primaryEngineRange']['remainingRangeInKm'], + 'carCapturedTimestamp': status_data['carCapturedTimestamp'].split('.')[0] + 'Z', + } + } + } + } diff --git a/packages/modules/vehicles/skoda/soc.py b/packages/modules/vehicles/skoda/soc.py new file mode 100755 index 0000000000..c2de41bb2d --- /dev/null +++ b/packages/modules/vehicles/skoda/soc.py @@ -0,0 +1,43 @@ +from typing import List + +import logging + +from helpermodules.cli import run_using_positional_cli_args +from modules.common import store +from modules.common.abstract_device import DeviceDescriptor +from modules.common.abstract_vehicle import VehicleUpdateData +from modules.common.component_state import CarState +from modules.common.configurable_vehicle import ConfigurableVehicle +from modules.vehicles.skoda import api +from modules.vehicles.skoda.config import Skoda, SkodaConfiguration + + +log = logging.getLogger(__name__) + + +def fetch(vehicle_update_data: VehicleUpdateData, config: Skoda, vehicle: int) -> CarState: + soc, range, soc_ts, soc_tsX = api.fetch_soc(config, vehicle) + log.info("Result: soc=" + str(soc)+", range=" + str(range) + "@" + soc_ts) + return CarState(soc=soc, range=range, soc_timestamp=soc_tsX) + + +def create_vehicle(vehicle_config: Skoda, vehicle: int): + def updater(vehicle_update_data: VehicleUpdateData) -> CarState: + return fetch(vehicle_update_data, vehicle_config, vehicle) + return ConfigurableVehicle(vehicle_config=vehicle_config, + component_updater=updater, + vehicle=vehicle, + calc_while_charging=vehicle_config.configuration.calculate_soc) + + +def skoda_update(user_id: str, password: str, vin: str, refreshToken: str, charge_point: int): + log.debug("skoda: user_id="+user_id+"vin="+vin+"charge_point="+str(charge_point)) + store.get_car_value_store(charge_point).store.set( + fetch(None, Skoda(configuration=SkodaConfiguration(user_id, password, vin, refreshToken)), charge_point)) + + +def main(argv: List[str]): + run_using_positional_cli_args(skoda_update, argv) + + +device_descriptor = DeviceDescriptor(configuration_factory=Skoda) diff --git a/packages/modules/vehicles/vwgroup/__init__.py b/packages/modules/vehicles/vwgroup/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/packages/modules/vehicles/vwid/socutils.py b/packages/modules/vehicles/vwgroup/socutils.py similarity index 100% rename from packages/modules/vehicles/vwid/socutils.py rename to packages/modules/vehicles/vwgroup/socutils.py diff --git a/packages/modules/vehicles/vwgroup/vwgroup.py b/packages/modules/vehicles/vwgroup/vwgroup.py new file mode 100644 index 0000000000..683baf2235 --- /dev/null +++ b/packages/modules/vehicles/vwgroup/vwgroup.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +from datetime import datetime +from json import dumps +import logging +from time import mktime, time +from typing import Union +from modules.common.store import RAMDISK_PATH +from modules.vehicles.vwgroup.socutils import socUtils + +date_fmt = '%Y-%m-%d %H:%M:%S' +ts_fmt = '%Y-%m-%dT%H:%M:%S' + +refreshToken_exp_days = 7 # 7 days before refreshToken expires a new refreshToken shall be stored +initialToken = '1.2.3' + + +class VwGroup(object): + def __init__(self, conf, vehicle): + self.log = logging.getLogger(__name__) + self.su = socUtils() + self.user_id = conf.configuration.user_id + self.password = conf.configuration.password + self.vin = conf.configuration.vin + self.refreshToken = conf.configuration.refreshToken + self.replyFile = 'soc_' + str(conf.type) + '_reply_vh_' + str(vehicle) + self.accessTokenFile = str(RAMDISK_PATH) + '/soc_' + str(conf.type) + '_accessToken_vh_' + str(vehicle) + self.accessToken_old = {} + self.vehicle = vehicle + self.conf = conf + + # convert utc timestamp to local time + def utc2local(self, utc): + epoch = mktime(utc.timetuple()) + offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) + return utc + offset + + # async method, called from sync fetch_soc, required because libvwid/libskoda expect async environment + async def request_data(self, library) -> Union[int, float, str]: + library.set_vin(self.vin) + library.set_credentials(self.user_id, self.password) + library.set_jobs(['charging']) + library.tokens = {} + library.headers = {} + + # initialize refreshToken + try: + if self.refreshToken is None: + self.log.debug("set refreshToken to initial value") + library.tokens['refreshToken'] = initialToken + else: + library.tokens['refreshToken'] = self.refreshToken + + except Exception: + self.log.debug("refreshToken initialization exception: set refreshToken_old to initial value") + library.tokens['refreshToken'] = initialToken + + self.refreshTokenOld = library.tokens['refreshToken'] # remember current refreshToken + + # initialize accessToken + self.accessTokenOld = self.su.read_token_file(self.accessTokenFile) + if self.accessTokenOld is None: + self.log.debug('set accessToken to initial value') + self.accessTokenOld = initialToken + library.tokens['accessToken'] = self.accessTokenOld # initialize tokens in vwid + library.headers['Authorization'] = 'Bearer %s' % library.tokens["accessToken"] + + # get status from VW server + self.data = await library.get_status() + if (self.data): + if self.su.keys_exist(self.data, 'userCapabilities', 'capabilitiesStatus', 'error'): + self.log.error("Server Error: \n" + + dumps(self.data['userCapabilities']['capabilitiesStatus']['error'], + ensure_ascii=False, indent=4)) + + if self.su.keys_exist(self.data, 'charging', 'batteryStatus'): + self.log.debug("batteryStatus: \n" + + dumps(self.data['charging']['batteryStatus'], + ensure_ascii=False, indent=4)) + + try: + self.soc = int(self.data['charging']['batteryStatus']['value']['currentSOC_pct']) + self.range = float(self.data['charging']['batteryStatus']['value']['cruisingRangeElectric_km']) + soc_tsZ = self.data['charging']['batteryStatus']['value']['carCapturedTimestamp'] + soc_tsdtZ = datetime.strptime(soc_tsZ, ts_fmt + "Z") + soc_tsdtL = self.utc2local(soc_tsdtZ) + self.soc_tsX = datetime.timestamp(soc_tsdtL) + self.soc_ts = datetime.strftime(soc_tsdtL, ts_fmt) + except Exception as e: + self.log.exception("soc/range/soc_ts field missing exception: e=" + str(e)) + self.soc = 0 + self.range = 0.0 + self.soc_ts = "" + self.soc_tsX = time() + + # decision logic - shall a new refreshToken be stored? + self.store_refreshToken = False + self.refreshTokenNew = library.tokens['refreshToken'] + + if self.refreshTokenOld != initialToken: + try: + self.expOld, self.expOld_dt = self.su.get_token_expiration(self.refreshTokenOld, date_fmt) + self.now = int(time()) + expirationThreshold = self.expOld - refreshToken_exp_days * 86400 + + if expirationThreshold < self.now: + self.log.debug('RefreshToken: expiration in less than ' + + str(refreshToken_exp_days) + ' days on ' + self.expOld_dt + ', store new token') + self.store_refreshToken = True + except Exception as e: + self.log.debug("refreshToken decode exception: e=" + str(e)) + self.store_refreshToken = True # no old refreshToken, store new refreshToken anyway + + else: + self.log.debug("Old refreshToken expires on " + self.expOld_dt + ", keep it") + elif self.refreshTokenNew != initialToken: + self.store_refreshToken = True # no old refreshToken, store new refreshToken anyway + + if self.store_refreshToken: # refreshToken needs to be stored in config json + try: + self.expNew, self.expNew_dt = self.su.get_token_expiration(self.refreshTokenNew, date_fmt) + self.log.debug("store new refreshToken, expires on " + self.expNew_dt) + except Exception as e: + self.log.debug("new refreshToken decode exception, e=" + str(e)) + self.log.debug("new refreshToken=" + str(self.refreshTokenNew)) + + confDict = self.conf.__dict__ + confDict.pop('name') + confDict['configuration'] = self.conf.configuration.__dict__ + self.su.write_token_mqtt( + "openWB/set/vehicle/" + self.vehicle + "/soc_module/config", + self.refreshTokenNew, + self.conf.__dict__) + + if (library.tokens['accessToken'] != self.accessTokenOld): # modified accessToken? + self.su.write_token_file(self.accessTokenFile, library.tokens['accessToken']) + + return self.soc, self.range, self.soc_ts, self.soc_tsX diff --git a/packages/modules/vehicles/vwid/api.py b/packages/modules/vehicles/vwid/api.py index bf4198c294..30d69946b9 100755 --- a/packages/modules/vehicles/vwid/api.py +++ b/packages/modules/vehicles/vwid/api.py @@ -1,151 +1,23 @@ #!/usr/bin/env python3 -from logging import getLogger -from typing import Union -from modules.vehicles.vwid import libvwid import aiohttp from asyncio import new_event_loop, set_event_loop -from time import time, mktime -from datetime import datetime -from json import dumps -from modules.common.store import RAMDISK_PATH +from typing import Union +from modules.vehicles.vwid import libvwid from modules.vehicles.vwid.config import VWId -from modules.vehicles.vwid.socutils import socUtils - -date_fmt = '%Y-%m-%d %H:%M:%S' -ts_fmt = '%Y-%m-%dT%H:%M:%S' -refreshToken_exp_days = 7 # 7 days before refreshToken expires a new refreshToken shall be stored -initialToken = '1.2.3' - -log = getLogger(__name__) +from modules.vehicles.vwgroup.vwgroup import VwGroup -# convert utc timestamp to local time -def utc2local(utc): - epoch = mktime(utc.timetuple()) - offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) - return utc + offset +class api(VwGroup): + def __init__(self, conf: VWId, vehicle: int): + super().__init__(conf, vehicle) -class api: - - def __init__(self): - self.su = socUtils() - pass - - # async method, called from sync fetch_soc, required because libvwid expects async environment - async def _fetch_soc(self, - conf: VWId, - vehicle: int) -> Union[int, float, str]: - self.user_id = conf.configuration.user_id - self.password = conf.configuration.password - self.vin = conf.configuration.vin - self.refreshToken = conf.configuration.refreshToken - self.replyFile = 'soc_vwid_reply_vh_' + str(vehicle) - self.accessTokenFile = str(RAMDISK_PATH) + '/soc_vwid_accessToken_vh_' + str(vehicle) - self.accessToken_old = {} - + # async method, called from sync fetch_soc, required because libvwid expect async environment + async def _fetch_soc(self) -> Union[int, float, str]: async with aiohttp.ClientSession() as self.session: - self.w = libvwid.vwid(self.session) - self.w.set_vin(self.vin) - self.w.set_credentials(self.user_id, self.password) - self.w.set_jobs(['charging']) - self.w.tokens = {} - self.w.headers = {} - - # initialize refreshToken - try: - if self.refreshToken is None: - log.debug("set refreshToken to initial value") - self.w.tokens['refreshToken'] = initialToken - else: - self.w.tokens['refreshToken'] = self.refreshToken - - except Exception: - log.debug("refreshToken initialization exception: set refreshToken_old to initial value") - self.w.tokens['refreshToken'] = initialToken - - self.refreshTokenOld = self.w.tokens['refreshToken'] # remember current refreshToken - - # initialize accessToken - self.accessTokenOld = self.su.read_token_file(self.accessTokenFile) - if self.accessTokenOld is None: - log.debug('set accessToken to initial value') - self.accessTokenOld = initialToken - self.w.tokens['accessToken'] = self.accessTokenOld # initialize tokens in vwid - self.w.headers['Authorization'] = 'Bearer %s' % self.w.tokens["accessToken"] - - # get status from VW server - self.data = await self.w.get_status() - if (self.data): - if self.su.keys_exist(self.data, 'userCapabilities', 'capabilitiesStatus', 'error'): - log.error("Server Error: \n" - + dumps(self.data['userCapabilities']['capabilitiesStatus']['error'], - ensure_ascii=False, indent=4)) - - if self.su.keys_exist(self.data, 'charging', 'batteryStatus'): - log.debug("batteryStatus: \n" + - dumps(self.data['charging']['batteryStatus'], - ensure_ascii=False, indent=4)) - - try: - self.soc = int(self.data['charging']['batteryStatus']['value']['currentSOC_pct']) - self.range = float(self.data['charging']['batteryStatus']['value']['cruisingRangeElectric_km']) - soc_tsZ = self.data['charging']['batteryStatus']['value']['carCapturedTimestamp'] - soc_tsdtZ = datetime.strptime(soc_tsZ, ts_fmt + "Z") - soc_tsdtL = utc2local(soc_tsdtZ) - self.soc_tsX = datetime.timestamp(soc_tsdtL) - self.soc_ts = datetime.strftime(soc_tsdtL, ts_fmt) - except Exception as e: - log.exception("soc/range/soc_ts field missing exception: e=" + str(e)) - self.soc = 0 - self.range = 0.0 - self.soc_ts = "" - self.soc_tsX = time() - - # decision logic - shall a new refreshToken be stored? - self.store_refreshToken = False - self.refreshTokenNew = self.w.tokens['refreshToken'] - - if self.refreshTokenOld != initialToken: - try: - self.expOld, self.expOld_dt = self.su.get_token_expiration(self.refreshTokenOld, date_fmt) - self.now = int(time()) - expirationThreshold = self.expOld - refreshToken_exp_days * 86400 - - if expirationThreshold < self.now: - log.debug('RefreshToken: expiration in less than ' + - str(refreshToken_exp_days) + ' days on ' + self.expOld_dt + ', store new token') - self.store_refreshToken = True - except Exception as e: - log.debug("refreshToken decode exception: e=" + str(e)) - self.store_refreshToken = True # no old refreshToken, store new refreshToken anyway - - else: - log.debug("Old refreshToken expires on " + self.expOld_dt + ", keep it") - else: - self.store_refreshToken = True # no old refreshToken, store new refreshToken anyway - - if self.store_refreshToken: # refreshToken needs to be stored in config json - try: - self.expNew, self.expNew_dt = self.su.get_token_expiration(self.refreshTokenNew, date_fmt) - log.debug("store new refreshToken, expires on " + self.expNew_dt) - except Exception as e: - log.debug("new refreshToken decode exception, e=" + str(e)) - log.debug("new refreshToken=" + str(self.refreshTokenNew)) - - confDict = conf.__dict__ - confDict.pop('name') - confDict['configuration'] = conf.configuration.__dict__ - self.su.write_token_mqtt( - "openWB/set/vehicle/" + vehicle + "/soc_module/config", - self.refreshTokenNew, - conf.__dict__) - - if (self.w.tokens['accessToken'] != self.accessTokenOld): # modified accessToken? - self.su.write_token_file(self.accessTokenFile, self.w.tokens['accessToken']) - - return self.soc, self.range, self.soc_ts, self.soc_tsX + vwid = libvwid.vwid(self.session) + return await super().request_data(vwid) def fetch_soc(conf: VWId, vehicle: int) -> Union[int, float, str]: @@ -155,7 +27,7 @@ def fetch_soc(conf: VWId, vehicle: int) -> Union[int, float, str]: set_event_loop(loop) # get soc, range from server - a = api() - soc, range, soc_ts, soc_tsX = loop.run_until_complete(a._fetch_soc(conf, vehicle)) + a = api(conf, vehicle) + soc, range, soc_ts, soc_tsX = loop.run_until_complete(a._fetch_soc()) return soc, range, soc_ts, soc_tsX