From fa720bd94ed74323abee37ba9dd53da93b41d8aa Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 28 Nov 2023 15:45:28 +0100 Subject: [PATCH 01/41] reworked authentication and many properties --- requirements.txt | 1 + volkswagencarnet/vw_connection.py | 162 +++++-- volkswagencarnet/vw_const.py | 11 +- volkswagencarnet/vw_vehicle.py | 753 +++++++++++++----------------- 4 files changed, 443 insertions(+), 484 deletions(-) diff --git a/requirements.txt b/requirements.txt index bfbe4fa3..b3891bb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiohttp beautifulsoup4 +cryptography lxml pyjwt diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 671649d3..97356e7e 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -29,6 +29,7 @@ HEADERS_SESSION, HEADERS_AUTH, BASE_SESSION, + BASE_API, BASE_AUTH, CLIENT, XCLIENT_ID, @@ -106,29 +107,27 @@ async def doLogin(self, tries: int = 1): self._session_logged_in = True # Get VW-Group API tokens - if not await self._getAPITokens(): - self._session_logged_in = False - return False + # if not await self._getAPITokens(): + # self._session_logged_in = False + # return False # Get list of vehicles from account _LOGGER.debug("Fetching vehicles associated with account") - await self.set_token("vwg") + # await self.set_token("vwg") self._session_headers.pop("Content-Type", None) - loaded_vehicles = await self.get( - url=f"https://msg.volkswagen.de/fs-car/usermanagement/users/v1/{BRAND}/{self._session_country}/vehicles" - ) + loaded_vehicles = await self.get(url=f"{BASE_API}/vehicle/v2/vehicles") # Add Vehicle class object for all VIN-numbers from account - if loaded_vehicles.get("userVehicles") is not None: + if loaded_vehicles.get("data") is not None: _LOGGER.debug("Found vehicle(s) associated with account.") - for vehicle in loaded_vehicles.get("userVehicles").get("vehicle"): - self._vehicles.append(Vehicle(self, vehicle)) + for vehicle in loaded_vehicles.get("data"): + self._vehicles.append(Vehicle(self, vehicle.get("vin"))) else: _LOGGER.warning("Failed to login to We Connect API.") self._session_logged_in = False return False # Update all vehicles data before returning - await self.set_token("vwg") + # await self.set_token("vwg") await self.update() return True @@ -165,16 +164,18 @@ def base64URLEncode(s): self._session_auth_headers = HEADERS_AUTH.copy() if self._session_fulldebug: _LOGGER.debug("Requesting openid config") - req = await self._session.get(url="https://identity.vwgroup.io/.well-known/openid-configuration") + req = await self._session.get(url=f"{BASE_API}/login/v1/idk/openid-configuration") if req.status != 200: _LOGGER.debug("OpenId config error") return False response_data = await req.json() authorization_endpoint = response_data["authorization_endpoint"] + token_endpoint = response_data["token_endpoint"] auth_issuer = response_data["issuer"] # Get authorization page (login page) # https://identity.vwgroup.io/oidc/v1/authorize?nonce={NONCE}&state={STATE}&response_type={TOKEN_TYPES}&scope={SCOPE}&redirect_uri={APP_URI}&client_id={CLIENT_ID} + # https://identity.vwgroup.io/oidc/v1/authorize?client_id={CLIENT_ID}&scope={SCOPE}&response_type={TOKEN_TYPES}&redirect_uri={APP_URI} if self._session_fulldebug: _LOGGER.debug(f'Get authorization page from "{authorization_endpoint}"') self._session_auth_headers.pop("Referer", None) @@ -186,7 +187,6 @@ def base64URLEncode(s): raise ValueError("Verifier too short. n_bytes must be > 30.") elif len(code_verifier) > 128: raise ValueError("Verifier too long. n_bytes must be < 97.") - challenge = base64URLEncode(hashlib.sha256(code_verifier).digest()) req = await self._session.get( url=authorization_endpoint, @@ -194,11 +194,11 @@ def base64URLEncode(s): allow_redirects=False, params={ "redirect_uri": APP_URI, - "prompt": "login", - "nonce": getNonce(), - "state": getNonce(), - "code_challenge_method": "s256", - "code_challenge": challenge.decode(), + # "prompt": "login", + # "nonce": getNonce(), + # "state": getNonce(), + # "code_challenge_method": "s256", + # "code_challenge": challenge.decode(), "response_type": CLIENT[client].get("TOKEN_TYPES"), "client_id": CLIENT[client].get("CLIENT_ID"), "scope": CLIENT[client].get("SCOPE"), @@ -222,7 +222,7 @@ def base64URLEncode(s): ) else: _LOGGER.warning("Unable to fetch authorization endpoint.") - raise Exception('Missing "location" header') + raise Exception(f'Missing "location" header, payload returned: {await req.content.read()}') except Exception as error: _LOGGER.warning("Failed to get authorization endpoint") raise error @@ -329,23 +329,24 @@ def base64URLEncode(s): _LOGGER.debug("Login successful, received authorization code.") # Extract code and tokens - parsed_qs = parse_qs(urlparse(ref).fragment) + parsed_qs = parse_qs(urlparse(ref).query) jwt_auth_code = parsed_qs["code"][0] - jwt_id_token = parsed_qs["id_token"][0] + # jwt_id_token = parsed_qs["id_token"][0] # Exchange Auth code and id_token for new tokens with refresh_token (so we can easier fetch new ones later) token_body = { - "auth_code": jwt_auth_code, - "id_token": jwt_id_token, - "code_verifier": code_verifier.decode(), - "brand": BRAND, + "client_id": CLIENT[client].get("CLIENT_ID"), + "grant_type": "authorization_code", + "code": jwt_auth_code, + "redirect_uri": APP_URI + # "brand": BRAND, } _LOGGER.debug("Trying to fetch user identity tokens.") - token_url = "https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode" + token_url = token_endpoint req = await self._session.post( url=token_url, headers=self._session_auth_headers, data=token_body, allow_redirects=False ) if req.status != 200: - raise Exception("Token exchange failed") + raise Exception(f"Token exchange failed. Received message: {await req.content.read()}") # Save tokens as "identity", these are tokens representing the user self._session_tokens[client] = await req.json() if "error" in self._session_tokens[client]: @@ -367,6 +368,7 @@ def base64URLEncode(s): _LOGGER.exception(error) self._session_logged_in = False return False + self._session_headers["Authorization"] = "Bearer " + self._session_tokens[client]["access_token"] return True async def _getAPITokens(self): @@ -541,6 +543,7 @@ async def post(self, url, vin="", tries=0, **data): # Construct URL from request, home region and variables def _make_url(self, ref, vin=""): + return ref replacedUrl = re.sub("\\$vin", vin, ref) if "://" in replacedUrl: # already server contained in URL @@ -583,7 +586,9 @@ async def getHomeRegion(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") + # TODO: handle multiple home regions! (no examples available currently) + return True response = await self.get( "https://mal-1a.prd.ece.vwg-connect.com/api/cs/vds/v1/vehicles/$vin/homeRegion", vin ) @@ -604,10 +609,10 @@ async def getOperationList(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") - response = await self.get("/api/rolesrights/operationlist/v3/vehicles/$vin", vin) - if response.get("operationList", False): - data = response.get("operationList", {}) + # await self.set_token("vwg") + response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", "") + if response.get("capabilities", False): + data = response.get("capabilities", {}) elif response.get("status_code", {}): _LOGGER.warning(f'Could not fetch operation list, HTTP status code: {response.get("status_code")}') data = response @@ -619,6 +624,61 @@ async def getOperationList(self, vin): data = {"error": "unknown"} return data + async def getSelectiveStatus(self, vin, services): + """Get status information for specified services.""" + if not await self.validate_tokens: + return False + try: + response = await self.get( + f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs={','.join(services)}", "" + ) + + for service in services: + if not response.get(service): + _LOGGER.debug( + f"Did not receive return data for requested service {service}. (This is expected for several service/car combinations)" + ) + + return response + + except Exception as error: + _LOGGER.warning(f"Could not fetch selectivestatus, error: {error}") + return False + + async def getVehicleData(self, vin): + """Get car information like VIN, nickname, etc.""" + if not await self.validate_tokens: + return False + try: + response = await self.get(f"{BASE_API}/vehicle/v2/vehicles", "") + + for vehicle in response.get("data"): + if vehicle.get("vin") == vin: + data = {"vehicle": vehicle} + return data + + _LOGGER.warning(f"Could not fetch vehicle data for vin {vin}") + + except Exception as error: + _LOGGER.warning(f"Could not fetch vehicle data, error: {error}") + return False + + async def getParkingPosition(self, vin): + """Get information about the parking position.""" + if not await self.validate_tokens: + return False + try: + response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", "") + + if "data" in response: + return {"parkingposition": response["data"]} + + _LOGGER.warning(f"Could not fetch parkingposition for vin {vin}") + + except Exception as error: + _LOGGER.warning(f"Could not fetch parkingposition, error: {error}") + return False + async def getRealCarData(self, vin): """Get car information from customer profile, VIN, nickname, etc.""" if not await self.validate_tokens: @@ -629,7 +689,7 @@ async def getRealCarData(self, vin): subject = jwt.decode(atoken, options={"verify_signature": False}, algorithms=JWT_ALGORITHMS).get( "sub", None ) - await self.set_token("identity") + # await self.set_token("identity") self._session_headers["Accept"] = "application/json" response = await self.get(f"https://customer-profile.vwgroup.io/v1/customers/{subject}/realCarData") if response.get("realCars", {}): @@ -652,7 +712,7 @@ async def getCarportData(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") self._session_headers["Accept"] = ( "application/vnd.vwg.mbb.vehicleDataDetail_v2_1_0+json," " application/vnd.vwg.mbb.genericError_v1_0_2+json" @@ -676,7 +736,7 @@ async def getCarportData(self, vin): async def getVehicleStatusData(self, vin): """Get stored vehicle data response.""" try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get(f"fs-car/bs/vsr/v1/{BRAND}/{self._session_country}/vehicles/$vin/status", vin=vin) if ( response.get("StoredVehicleDataResponse", {}) @@ -708,7 +768,7 @@ async def getTripStatistics(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get( f"fs-car/bs/tripstatistics/v1/{BRAND}/{self._session_country}/vehicles/$vin/tripdata/shortTerm?newest", vin=vin, @@ -729,7 +789,7 @@ async def getPosition(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get( f"fs-car/bs/cf/v1/{BRAND}/{self._session_country}/vehicles/$vin/position", vin=vin ) @@ -754,7 +814,7 @@ async def getTimers(self, vin) -> TimerData | None: if not await self.validate_tokens: return None try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get( f"fs-car/bs/departuretimer/v1/{BRAND}/{self._session_country}/vehicles/$vin/timer", vin=vin ) @@ -774,7 +834,7 @@ async def getClimater(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get( f"fs-car/bs/climatisation/v1/{BRAND}/{self._session_country}/vehicles/$vin/climater", vin=vin ) @@ -794,7 +854,7 @@ async def getCharger(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get( f"fs-car/bs/batterycharge/v1/{BRAND}/{self._session_country}/vehicles/$vin/charger", vin=vin ) @@ -814,7 +874,7 @@ async def getPreHeater(self, vin): if not await self.validate_tokens: return False try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.get(f"fs-car/bs/rs/v1/{BRAND}/{self._session_country}/vehicles/$vin/status", vin=vin) if response.get("statusResponse", {}): data = {"heating": response.get("statusResponse", {})} @@ -839,7 +899,7 @@ async def get_request_status(self, vin, sectionId, requestId): if not await self.doLogin(): _LOGGER.warning(f"Login for {BRAND} account failed!") raise Exception(f"Login for {BRAND} account failed") - await self.set_token("vwg") + # await self.set_token("vwg") if sectionId == "climatisation": url = ( f"fs-car/bs/$sectionId/v1/{BRAND}/{self._session_country}/vehicles/$vin/climater/actions/$requestId" @@ -964,7 +1024,7 @@ async def dataCall(self, query, vin="", **data): async def setRefresh(self, vin): """Force vehicle data update.""" try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/vsr/v1/{BRAND}/{self._session_country}/vehicles/$vin/requests", vin, data=None ) @@ -987,7 +1047,7 @@ async def setRefresh(self, vin): async def setCharger(self, vin, data) -> dict[str, str | int | None]: """Start/Stop charger.""" try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/batterycharge/v1/{BRAND}/{self._session_country}/vehicles/$vin/charger/actions", vin, @@ -1012,7 +1072,7 @@ async def setCharger(self, vin, data) -> dict[str, str | int | None]: async def setClimater(self, vin, data, spin): """Execute climatisation actions.""" try: - await self.set_token("vwg") + # await self.set_token("vwg") # Only get security token if auxiliary heater is to be started if data.get("action", {}).get("settings", {}).get("heaterSource", None) == "auxiliary": self._session_headers["X-securityToken"] = await self.get_sec_token(vin=vin, spin=spin, action="rclima") @@ -1043,7 +1103,7 @@ async def setPreHeater(self, vin, data, spin): """Petrol/diesel parking heater actions.""" content_type = None try: - await self.set_token("vwg") + # await self.set_token("vwg") if "Content-Type" in self._session_headers: content_type = self._session_headers["Content-Type"] else: @@ -1103,7 +1163,7 @@ async def setChargeMinLevel(self, vin: str, limit: int): async def _setDepartureTimer(self, vin, data: TimersAndProfiles, action: str): """Set schedules.""" try: - await self.set_token("vwg") + # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/departuretimer/v1/{BRAND}/{self._session_country}/vehicles/$vin/timer/actions", vin=vin, @@ -1137,7 +1197,7 @@ async def setLock(self, vin, data, spin): """Remote lock and unlock actions.""" content_type = None try: - await self.set_token("vwg") + # await self.set_token("vwg") # Prepare data, headers and fetch security token if "Content-Type" in self._session_headers: content_type = self._session_headers["Content-Type"] @@ -1181,7 +1241,7 @@ async def setLock(self, vin, data, spin): async def validate_tokens(self): """Validate expiry of tokens.""" idtoken = self._session_tokens["identity"]["id_token"] - atoken = self._session_tokens["vwg"]["access_token"] + atoken = self._session_tokens["identity"]["access_token"] id_exp = jwt.decode( idtoken, options={"verify_signature": False, "verify_aud": False}, algorithms=JWT_ALGORITHMS ).get("exp", None) @@ -1212,7 +1272,7 @@ async def validate_tokens(self): async def verify_tokens(self, token, type, client="Legacy"): """Verify JWT against JWK(s).""" if type == "identity": - req = await self._session.get(url="https://identity.vwgroup.io/oidc/v1/keys") + req = await self._session.get(url="https://identity.vwgroup.io/v1/jwks") keys = await req.json() audience = [ CLIENT[client].get("CLIENT_ID"), @@ -1260,7 +1320,7 @@ async def refresh_tokens(self): body = { "grant_type": "refresh_token", - "brand": BRAND, + # "brand": BRAND, "refresh_token": self._session_tokens["identity"]["refresh_token"], } response = await self._session.post( diff --git a/volkswagencarnet/vw_const.py b/volkswagencarnet/vw_const.py index 1a8212e3..01d42f49 100644 --- a/volkswagencarnet/vw_const.py +++ b/volkswagencarnet/vw_const.py @@ -2,17 +2,18 @@ BASE_SESSION = "https://msg.volkswagen.de" BASE_AUTH = "https://identity.vwgroup.io" +BASE_API = "https://emea.bff.cariad.digital" BRAND = "VW" COUNTRY = "DE" # Data used in communication CLIENT = { "Legacy": { - "CLIENT_ID": "9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com", + "CLIENT_ID": "a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com", # client id for VWG API, legacy Skoda Connect/MySkoda - "SCOPE": "openid mbb profile cars address email birthdate nickname phone", + "SCOPE": "openid profile badge cars dealers vin", # 'SCOPE': 'openid mbb profile cars address email birthdate badge phone driversLicense dealers profession vin', - "TOKEN_TYPES": "code id_token token", + "TOKEN_TYPES": "code", # id_token token", }, "New": { "CLIENT_ID": "f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com", @@ -31,8 +32,8 @@ XCLIENT_ID = "c8fcb3bf-22d3-44b0-b6ce-30eae0a4986f" XAPPVERSION = "5.3.2" XAPPNAME = "We Connect" -USER_AGENT = "OneConnect/000000148 CFNetwork/1485 Darwin/23.1.0" -APP_URI = "carnet://identity-kit/login" +USER_AGENT = "Volkswagen/2.20.0 iOS/17.1.1" +APP_URI = "weconnect://authenticated" # Used when fetching data HEADERS_SESSION = { diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 8a6b4d80..f7756edb 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -13,6 +13,13 @@ from .vw_const import VehicleStatusParameter as P from .vw_utilities import find_path, is_valid_path +# TODO +# Images (https://emea.bff.cariad.digital/media/v2/vehicle-images/WVWZZZ3HZPK002581?resolution=3x) +# {"data":[{"id":"door_right_front_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_right_front_overlay.png","fileName":"image_door_right_front_overlay.png"},{"id":"light_right","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_light_right.png","fileName":"image_light_right.png"},{"id":"sunroof_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_sunroof_overlay.png","fileName":"image_sunroof_overlay.png"},{"id":"trunk_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_trunk_overlay.png","fileName":"image_trunk_overlay.png"},{"id":"car_birdview","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_car_birdview.png","fileName":"image_car_birdview.png"},{"id":"door_left_front","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_left_front.png","fileName":"image_door_left_front.png"},{"id":"door_right_front","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_right_front.png","fileName":"image_door_right_front.png"},{"id":"sunroof","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_sunroof.png","fileName":"image_sunroof.png"},{"id":"window_right_front_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_window_right_front_overlay.png","fileName":"image_window_right_front_overlay.png"},{"id":"car_34view","url":"https://media.volkswagen.com/Vilma/V/3H9/2023/Front_Right/c8ca31fcf999b04d42940620653c494215e0d49756615f3524499261d96ccdce.png?width=1163","fileName":""},{"id":"door_left_back","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_left_back.png","fileName":"image_door_left_back.png"},{"id":"door_right_back","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_right_back.png","fileName":"image_door_right_back.png"},{"id":"window_left_back_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_window_left_back_overlay.png","fileName":"image_window_left_back_overlay.png"},{"id":"window_right_back_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_window_right_back_overlay.png","fileName":"image_window_right_back_overlay.png"},{"id":"bonnet_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_bonnet_overlay.png","fileName":"image_bonnet_overlay.png"},{"id":"door_left_back_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_left_back_overlay.png","fileName":"image_door_left_back_overlay.png"},{"id":"door_left_front_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_left_front_overlay.png","fileName":"image_door_left_front_overlay.png"},{"id":"door_right_back_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_door_right_back_overlay.png","fileName":"image_door_right_back_overlay.png"},{"id":"light_left","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_light_left.png","fileName":"image_light_left.png"},{"id":"window_left_front_overlay","url":"https://emea.bff.cariad.digital/media/v2/image/arteon_shooting_brake/3x/image_window_left_front_overlay.png","fileName":"image_window_left_front_overlay.png"}]} +# +# Model Year (unclear, seems to only be available via the web API, language dependent and with separate authentication) + + BACKEND_RECEIVED_TIMESTAMP = "BACKEND_RECEIVED_TIMESTAMP" LOCKED_STATE = 2 @@ -21,9 +28,11 @@ _LOGGER = logging.getLogger(__name__) +# TODO find out ENGINE_TYPE_ELECTRIC = "3" -ENGINE_TYPE_DIESEL = "5" -ENGINE_TYPE_GASOLINE = "6" +ENGINE_TYPE_DIESEL = "diesel" +# TODO verify +ENGINE_TYPE_GASOLINE = "gasoline" ENGINE_TYPE_COMBUSTION = [ENGINE_TYPE_DIESEL, ENGINE_TYPE_GASOLINE] UNSUPPORTED = 0 @@ -33,10 +42,10 @@ class Vehicle: """Vehicle contains the state of sensors and methods for interacting with the car.""" - def __init__(self, conn, url): + def __init__(self, conn, vin): """Initialize the Vehicle with default values.""" self._connection = conn - self._url = url + self._vin = vin self._homeregion = "https://msg.volkswagen.de" self._discovered = False self._states = {} @@ -56,18 +65,19 @@ def __init__(self, conn, url): # API Endpoints that might be enabled for car (that we support) self._services: dict[str, dict[str, Any]] = { - "rheating_v1": {"active": False}, - "rclima_v1": {"active": False}, - "rlu_v1": {"active": False}, - "trip_statistic_v1": {"active": False}, - "statusreport_v1": {"active": False}, - "rbatterycharge_v1": {"active": False}, - "rhonk_v1": {"active": False}, - "carfinder_v1": {"active": False}, - "timerprogramming_v1": {"active": False}, - # "jobs_v1": {"active": False}, - # "owner_v1": {"active": False}, - # vehicles_v1_cai, services_v1, vehicletelemetry_v1 + # "rheating_v1": {"active": False}, # TODO: equivalent in new API unknown + # "rclima_v1": {"active": False}, # TODO: equivalent in new API unknown + "access": {"active": False}, + "tripStatistics": {"active": False}, + "measurements": {"active": False}, + # "statusreport_v1": {"active": False}, # TODO: equivalent in new API unknown, potentially "state"? + # "rbatterycharge_v1": {"active": False}, # TODO: equivalent in new API unknown + "honkAndFlash": {"active": False}, + # "carfinder_v1": {"active": False}, # TODO: equivalent in new API unknown + # "timerprogramming_v1": {"active": False}, # TODO: equivalent in new API unknown + # "jobs_v1": {"active": False}, # TODO: equivalent in new API unknown + # "owner_v1": {"active": False}, # TODO: equivalent in new API unknown + # vehicles_v1_cai, services_v1, vehicletelemetry_v1 # TODO: equivalent in new API unknown } def _in_progress(self, topic: str, unknown_offset: int = 0) -> bool: @@ -111,42 +121,39 @@ async def _handle_response(self, response, topic: str, error_msg: str | None = N # Init and update vehicle data async def discover(self): """Discover vehicle and initial data.""" - homeregion = await self._connection.getHomeRegion(self.vin) - _LOGGER.debug(f"Get homeregion for VIN {self.vin}") - if homeregion: - self._homeregion = homeregion + # homeregion = await self._connection.getHomeRegion(self.vin) + # _LOGGER.debug(f"Get homeregion for VIN {self.vin}") + # if homeregion: + # self._homeregion = homeregion - await asyncio.gather(self.get_carportdata(), self.get_realcardata(), return_exceptions=True) - _LOGGER.info(f'Vehicle {self.vin} added. Homeregion is "{self._homeregion}"') + # await asyncio.gather(self.get_carportdata(), self.get_realcardata(), return_exceptions=True) + # _LOGGER.info(f'Vehicle {self.vin} added. Homeregion is "{self._homeregion}"') _LOGGER.debug("Attempting discovery of supported API endpoints for vehicle.") operation_list = await self._connection.getOperationList(self.vin) if operation_list: - service_info = operation_list["serviceInfo"] + # service_info = operation_list["serviceInfo"] # Iterate over all endpoints in ServiceInfo list - for service in service_info: + for service_id in operation_list.keys(): try: - if service.get("serviceId", "Invalid") in self._services.keys(): + if service_id in self._services.keys(): + service = operation_list[service_id] data = {} - service_name = service.get("serviceId", None) - if service.get("serviceStatus", {}).get("status", "Disabled") == "Enabled": - _LOGGER.debug(f'Discovered enabled service: {service["serviceId"]}') + service_name = service.get("id", None) + if service.get("isEnabled", False): + _LOGGER.debug(f"Discovered enabled service: {service_name}") data["active"] = True - if service.get("cumulatedLicense", {}).get("expirationDate", False): - data["expiration"] = ( - service.get("cumulatedLicense", {}).get("expirationDate", None).get("content", None) - ) - if service.get("operation", False): + if service.get("expirationDate", False): + data["expiration"] = service.get("expirationDate", None) + if service.get("operations", False): data.update({"operations": []}) - for operation in service.get("operation", []): + for operation_id in service.get("operations", []).keys(): + operation = service.get("operations").get(operation_id) data["operations"].append(operation.get("id", None)) - elif service.get("serviceStatus", {}).get("status", None) == "Disabled": - reason = service.get("serviceStatus", {}).get("reason", "Unknown") + else: + reason = service.get("status", "Unknown") _LOGGER.debug(f"Service: {service_name} is disabled because of reason: {reason}") data["active"] = False - else: - _LOGGER.warning(f"Could not determine status of service: {service_name}, assuming enabled") - data["active"] = True self._services[service_name].update(data) except Exception as error: _LOGGER.warning(f'Encountered exception: "{error}" while parsing service item: {service}') @@ -161,19 +168,55 @@ async def update(self): await self.discover() if not self.deactivated: await asyncio.gather( - self.get_preheater(), - self.get_climater(), - self.get_trip_statistic(), - self.get_position(), - self.get_statusreport(), - self.get_charger(), - self.get_timerprogramming(), - return_exceptions=True, + # TODO: we don't check against capabilities currently, but this also doesn't seem to be neccesary + # to be checked if we should still do it for UI purposes + self.get_selectivestatus( + [ + "access", + "fuelStatus", + "honkAndFlash", + "userCapabilities", + "vehicleLights", + "vehicleHealthInspection", + "measurements", + "charging", + "climatisation", + "automation", + ] + ), + self.get_vehicle(), + self.get_parkingposition() + # self.get_preheater(), + # self.get_climater(), + # self.get_trip_statistic(), + # self.get_position(), + # self.get_statusreport(), + # self.get_charger(), + # self.get_timerprogramming(), + # return_exceptions=True, ) else: _LOGGER.info(f"Vehicle with VIN {self.vin} is deactivated.") # Data collection functions + async def get_selectivestatus(self, services): + """Fetch selective status for specified services.""" + data = await self._connection.getSelectiveStatus(self.vin, services) + if data: + self._states.update(data) + + async def get_vehicle(self): + """Fetch car masterdata.""" + data = await self._connection.getVehicleData(self.vin) + if data: + self._states.update(data) + + async def get_parkingposition(self): + """Fetch parking position.""" + data = await self._connection.getParkingPosition(self.vin) + if data: + self._states.update(data) + async def get_realcardata(self): """Fetch realcardata.""" data = await self._connection.getRealCarData(self.vin) @@ -620,7 +663,7 @@ def vin(self) -> str: :return: """ - return self._url + return self._vin @property def unique_id(self) -> str: @@ -640,7 +683,7 @@ def nickname(self) -> str | None: :return: """ - return self.attrs.get("carData", {}).get("nickname", None) + return self.attrs.get("vehicle", {}).get("nickname", None) @property def is_nickname_supported(self) -> bool: @@ -649,7 +692,7 @@ def is_nickname_supported(self) -> bool: :return: """ - return self.attrs.get("carData", {}).get("nickname", False) is not False + return self.attrs.get("vehicle", {}).get("nickname", False) is not False @property def deactivated(self) -> bool | None: @@ -672,22 +715,22 @@ def is_deactivated_supported(self) -> bool: @property def model(self) -> str | None: """Return model.""" - return self.attrs.get("carportData", {}).get("modelName", None) + return self.attrs.get("vehicle", {}).get("model", None) @property def is_model_supported(self) -> bool: """Return true if model is supported.""" - return self.attrs.get("carportData", {}).get("modelName", False) is not False + return self.attrs.get("vehicle", {}).get("modelName", False) is not False @property def model_year(self) -> bool | None: """Return model year.""" - return self.attrs.get("carportData", {}).get("modelYear", None) + return self.attrs.get("vehicle", {}).get("modelYear", None) @property def is_model_year_supported(self) -> bool: """Return true if model year is supported.""" - return self.attrs.get("carportData", {}).get("modelYear", False) is not False + return self.attrs.get("vehicle", {}).get("modelYear", False) is not False @property def model_image(self) -> str: @@ -709,32 +752,32 @@ def is_model_image_supported(self) -> bool: @property def parking_light(self) -> bool: """Return true if parking light is on.""" - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.PARKING_LIGHT].get("value", 0)) - return response != 2 + lights = self.attrs.get("vehicleLights").get("lightsStatus").get("value").get("lights") + lights_on_count = 0 + for light in lights: + if light["status"] == "on": + lights_on_count = lights_on_count + 1 + return lights_on_count == 1 @property def parking_light_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.PARKING_LIGHT].get(BACKEND_RECEIVED_TIMESTAMP) + return self.attrs.get("vehicleLights").get("lightsStatus").get("value").get("carCapturedTimestamp") @property def is_parking_light_supported(self) -> bool: """Return true if parking light is supported.""" - return self.attrs.get("StoredVehicleDataResponseParsed", False) and P.PARKING_LIGHT in self.attrs.get( - "StoredVehicleDataResponseParsed" - ) + return self.attrs.get("vehicleLights", False) and "lights" in self.attrs.get("vehicleLights").get( + "lightsStatus" + ).get("value") # Connection status @property def last_connected(self) -> str: """Return when vehicle was last connected to connect servers in local time.""" - last_connected_utc = ( - self.attrs.get("StoredVehicleDataResponse") - .get("vehicleData") - .get("data")[0] - .get("field")[0] - .get("tsCarSentUtc") - ) + # this field is only a dirty hack, because there is no overarching information for the car anymore, + # only information per service, so we just use the one for odometer + last_connected_utc = self.attrs.get("measurements").get("odometerStatus").get("carCapturedTimestamp")[0] last_connected = last_connected_utc.replace(tzinfo=timezone.utc).astimezone(tz=None) return last_connected.strftime("%Y-%m-%d %H:%M:%S") @@ -758,34 +801,27 @@ def is_last_connected_supported(self) -> bool: @property def distance(self) -> int | None: """Return vehicle odometer.""" - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.ODOMETER].get("value", 0) - if value: - return int(value) - return None + return find_path(self.attrs, "measurements.odometerStatus.value.odometer") @property def distance_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.ODOMETER].get("BACKEND_RECEIVED_TIMESTAMP") + return find_path(self.attrs, "measurements.odometerStatus.value.carCapturedTimestamp") @property def is_distance_supported(self) -> bool: """Return true if odometer is supported.""" - return self.attrs.get("StoredVehicleDataResponseParsed", False) and P.ODOMETER in self.attrs.get( - "StoredVehicleDataResponseParsed" - ) + return is_valid_path(self.attrs, "measurements.odometerStatus.value.odometer") @property def service_inspection(self): """Return time left for service inspection.""" - return -int(self.attrs.get("StoredVehicleDataResponseParsed")[P.DAYS_TO_SERVICE_INSPECTION].get("value")) + return int(find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.inspectionDue_days")) @property def service_inspection_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.DAYS_TO_SERVICE_INSPECTION].get( - BACKEND_RECEIVED_TIMESTAMP - ) + return find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def is_service_inspection_supported(self) -> bool: @@ -794,21 +830,17 @@ def is_service_inspection_supported(self) -> bool: :return: """ - return self.attrs.get( - "StoredVehicleDataResponseParsed", False - ) and P.DAYS_TO_SERVICE_INSPECTION in self.attrs.get("StoredVehicleDataResponseParsed") + return is_valid_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.inspectionDue_days") @property def service_inspection_distance(self): """Return distance left for service inspection.""" - return -int(self.attrs.get("StoredVehicleDataResponseParsed")[P.DISTANCE_TO_SERVICE_INSPECTION].get("value", 0)) + return int(find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.inspectionDue_km")) @property def service_inspection_distance_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.DISTANCE_TO_SERVICE_INSPECTION].get( - BACKEND_RECEIVED_TIMESTAMP - ) + return find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def is_service_inspection_distance_supported(self) -> bool: @@ -817,24 +849,17 @@ def is_service_inspection_distance_supported(self) -> bool: :return: """ - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.DISTANCE_TO_SERVICE_INSPECTION in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - return False + return is_valid_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def oil_inspection(self): """Return time left for oil inspection.""" - return -int( - self.attrs.get("StoredVehicleDataResponseParsed", {}).get(P.DAYS_TO_OIL_INSPECTION, {}).get("value", 0) - ) + return int(find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.oilServiceDue_days")) @property def oil_inspection_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.DAYS_TO_OIL_INSPECTION].get( - BACKEND_RECEIVED_TIMESTAMP - ) + return find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def is_oil_inspection_supported(self) -> bool: @@ -845,28 +870,17 @@ def is_oil_inspection_supported(self) -> bool: """ if not self.has_combustion_engine(): return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.DAYS_TO_OIL_INSPECTION in self.attrs.get("StoredVehicleDataResponseParsed"): - if ( - self.attrs.get("StoredVehicleDataResponseParsed").get(P.DAYS_TO_OIL_INSPECTION).get("value", None) - is not None - ): - return True - return False + return is_valid_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def oil_inspection_distance(self): """Return distance left for oil inspection.""" - return -int( - self.attrs.get("StoredVehicleDataResponseParsed", {}).get(P.DISTANCE_TO_OIL_INSPECTION, {}).get("value", 0) - ) + return int(find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.oilServiceDue_km")) @property def oil_inspection_distance_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.DISTANCE_TO_OIL_INSPECTION].get( - BACKEND_RECEIVED_TIMESTAMP - ) + return find_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.carCapturedTimestamp") @property def is_oil_inspection_distance_supported(self) -> bool: @@ -877,235 +891,137 @@ def is_oil_inspection_distance_supported(self) -> bool: """ if not self.has_combustion_engine(): return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.DISTANCE_TO_OIL_INSPECTION in self.attrs.get("StoredVehicleDataResponseParsed"): - if ( - self.attrs.get("StoredVehicleDataResponseParsed") - .get(P.DISTANCE_TO_OIL_INSPECTION) - .get("value", None) - is not None - ): - return True - return False + return is_valid_path(self.attrs, "vehicleHealthInspection.maintenanceStatus.value.oilServiceDue_km") @property def adblue_level(self) -> int: """Return adblue level.""" - return int(self.attrs.get("StoredVehicleDataResponseParsed", {}).get(P.ADBLUE_LEVEL, {}).get("value", 0)) + return int(find_path(self.attrs, "measurements.rangeStatus.value.adBlueRange")) @property def adblue_level_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.ADBLUE_LEVEL].get(BACKEND_RECEIVED_TIMESTAMP) + return find_path(self.attrs, "measurements.rangeStatus.value.carCapturedTimestamp") @property def is_adblue_level_supported(self) -> bool: """Return true if adblue level is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.ADBLUE_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): - if "value" in self.attrs.get("StoredVehicleDataResponseParsed")[P.ADBLUE_LEVEL]: - if self.attrs.get("StoredVehicleDataResponseParsed")[P.ADBLUE_LEVEL].get("value", 0) is not None: - return True - return False + return is_valid_path(self.attrs, "measurements.rangeStatus.value.adBlueRange") # Charger related states for EV and PHEV @property def charging(self) -> bool: """Return charging state.""" - cstate = ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("chargingStatusData", {}) - .get("chargingState", {}) - .get("content", "") - ) + cstate = find_path(self.attrs, "charging.chargingStatus.value.chargingState") return cstate == "charging" @property def charging_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("chargingStatusData", {}) - .get("chargingState", {}) - .get("timstamp") - ) + return find_path(self.attrs, "charging.chargingStatus.value.carCapturedTimestamp") @property def is_charging_supported(self) -> bool: """Return true if charging is supported.""" - if self.attrs.get("charger", False): - if "status" in self.attrs.get("charger", {}): - if "chargingStatusData" in self.attrs.get("charger")["status"]: - if "chargingState" in self.attrs.get("charger")["status"]["chargingStatusData"]: - return True - return False + return is_valid_path(self.attrs, "charging.chargingStatus.value.chargingState") @property def battery_level(self) -> int: """Return battery level.""" - return int( - self.attrs.get("charger").get("status").get("batteryStatusData").get("stateOfCharge").get("content", 0) - ) + return int(find_path(self.attrs, "charging.batteryStatus.value.currentSOC_pct")) @property def battery_level_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("charger").get("status").get("batteryStatusData").get("stateOfCharge").get("timestamp") + return find_path(self.attrs, "charging.batteryStatus.value.carCapturedTimestamp") @property def is_battery_level_supported(self) -> bool: """Return true if battery level is supported.""" - if self.attrs.get("charger", False): - if "status" in self.attrs.get("charger"): - if "batteryStatusData" in self.attrs.get("charger")["status"]: - if "stateOfCharge" in self.attrs.get("charger")["status"]["batteryStatusData"]: - return True - return False + return is_valid_path(self.attrs, "charging.batteryStatus.value.currentSOC_pct") @property def charge_max_ampere(self) -> str | int: """Return charger max ampere setting.""" - value = int(self.attrs.get("charger").get("settings").get("maxChargeCurrent").get("content")) - if value == 254: - return "Maximum" - if value == 252: - return "Reduced" - if value == 0: - return "Unknown" - else: - return value + value = find_path(self.attrs, "charging.chargingSettings.value.maxChargeCurrentAC") + return value @property def charge_max_ampere_last_updated(self) -> datetime: """Return charger max ampere last updated.""" - return self.attrs.get("charger").get("settings").get("maxChargeCurrent").get("timestamp") + return find_path(self.attrs, "charging.chargingSettings.value.carCapturedTimestamp") @property def is_charge_max_ampere_supported(self) -> bool: """Return true if Charger Max Ampere is supported.""" - if self.attrs.get("charger", False): - if "settings" in self.attrs.get("charger", {}): - if "maxChargeCurrent" in self.attrs.get("charger", {})["settings"]: - return True - return False + return is_valid_path(self.attrs, "charging.chargingSettings.value.maxChargeCurrentAC") @property def charging_cable_locked(self) -> bool: """Return plug locked state.""" - response = self.attrs.get("charger")["status"]["plugStatusData"]["lockState"].get("content", 0) + response = find_path(self.attrs, "charging.plugStatus.value.plugLockState") return response == "locked" @property def charging_cable_locked_last_updated(self) -> datetime: """Return plug locked state.""" - return self.attrs.get("charger")["status"]["plugStatusData"]["lockState"].get("timestamp") + return find_path(self.attrs, "charging.plugStatus.value.carCapturedTimestamp") @property def is_charging_cable_locked_supported(self) -> bool: """Return true if plug locked state is supported.""" - if self.attrs.get("charger", False): - if "status" in self.attrs.get("charger", {}): - if "plugStatusData" in self.attrs.get("charger").get("status", {}): - if "lockState" in self.attrs.get("charger")["status"].get("plugStatusData", {}): - return True - return False + return is_valid_path(self.attrs, "charging.plugStatus.value.plugLockState") @property def charging_cable_connected(self) -> bool: """Return plug connected state.""" - response = self.attrs.get("charger")["status"]["plugStatusData"]["plugState"].get("content", 0) + response = find_path(self.attrs, "charging.plugStatus.value.plugConnectionState") return response == "connected" @property def charging_cable_connected_last_updated(self) -> datetime: """Return plug connected state last updated.""" - return self.attrs.get("charger")["status"]["plugStatusData"]["plugState"].get("timestamp") + return find_path(self.attrs, "charging.plugStatus.value.carCapturedTimestamp") @property def is_charging_cable_connected_supported(self) -> bool: - """Return true if charging cable connected is supported.""" - if self.attrs.get("charger", False): - if "status" in self.attrs.get("charger", {}): - if "plugStatusData" in self.attrs.get("charger").get("status", {}): - if "plugState" in self.attrs.get("charger")["status"].get("plugStatusData", {}): - return True - return False + """Return true if supported.""" + return is_valid_path(self.attrs, "charging.plugStatus.value.plugConnectionState") @property def charging_time_left(self) -> int: """Return minutes to charging complete.""" - if self.external_power: - minutes = ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("batteryStatusData", {}) - .get("remainingChargingTime", {}) - .get("content", 0) - ) - if minutes: - try: - if minutes == -1: - return 0 - if minutes == 65535: - return 0 - return minutes - except Exception: - pass - return 0 + return int(find_path(self.attrs, "charging.chargingStatus.value.remainingChargingTimeToComplete_min")) @property def charging_time_left_last_updated(self) -> datetime: """Return minutes to charging complete last updated.""" - return ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("batteryStatusData", {}) - .get("remainingChargingTime", {}) - .get("timestamp") - ) + return find_path(self.attrs, "charging.chargingStatus.value.carCapturedTimestamp") @property def is_charging_time_left_supported(self) -> bool: """Return true if charging is supported.""" - return self.is_charging_supported + return is_valid_path(self.attrs, "charging.chargingStatus.value.remainingChargingTimeToComplete_min") @property def external_power(self) -> bool: """Return true if external power is connected.""" - check = ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("chargingStatusData", {}) - .get("externalPowerSupplyState", {}) - .get("content", "") - ) - return check in ["stationConnected", "available"] + check = find_path(self.attrs, "charging.plugStatus.value.externalPower") + return check in ["stationConnected", "available", "ready"] @property def external_power_last_updated(self) -> datetime: """Return external power last updated.""" - return ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("chargingStatusData", {}) - .get("externalPowerSupplyState", {}) - .get("timestamp") - ) + return find_path(self.attrs, "charging.plugStatus.value.carCapturedTimestamp") @property def is_external_power_supported(self) -> bool: """External power supported.""" - return ( - self.attrs.get("charger", {}) - .get("status", {}) - .get("chargingStatusData", {}) - .get("externalPowerSupplyState", False) - ) + return is_valid_path(self.attrs, "charging.plugStatus.value.externalPower") @property def energy_flow(self): + # TODO untouched """Return true if energy is flowing through charging port.""" check = ( self.attrs.get("charger", {}) @@ -1118,6 +1034,7 @@ def energy_flow(self): @property def energy_flow_last_updated(self) -> datetime: + # TODO untouched """Return energy flow last updated.""" return ( self.attrs.get("charger", {}) @@ -1129,6 +1046,7 @@ def energy_flow_last_updated(self) -> datetime: @property def is_energy_flow_supported(self) -> bool: + # TODO untouched """Energy flow supported.""" return self.attrs.get("charger", {}).get("status", {}).get("chargingStatusData", {}).get("energyFlow", False) @@ -1203,23 +1121,12 @@ def electric_range(self) -> int: :return: """ - value = NO_VALUE - if self.is_primary_drive_electric(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_RANGE].get("value", UNSUPPORTED) - - elif self.is_secondary_drive_electric(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_RANGE].get("value", UNSUPPORTED) - return int(value) + return int(find_path(self.attrs, "measurements.rangeStatus.value.electricRange")) @property def electric_range_last_updated(self) -> datetime: """Return electric range last updated.""" - if self.is_primary_drive_electric(): - return self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_RANGE].get(BACKEND_RECEIVED_TIMESTAMP) - - elif self.is_secondary_drive_electric(): - return self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_RANGE].get(BACKEND_RECEIVED_TIMESTAMP) - raise ValueError() + return find_path(self.attrs, "measurements.rangeStatus.value.carCapturedTimestamp") @property def is_electric_range_supported(self) -> bool: @@ -1228,14 +1135,7 @@ def is_electric_range_supported(self) -> bool: :return: """ - supported = False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if self.is_primary_drive_electric(): - supported = True - - elif self.is_secondary_drive_electric(): - supported = True - return supported + return is_valid_path(self.attrs, "measurements.rangeStatus.value.electricRange") @property def combustion_range(self) -> int: @@ -1244,23 +1144,18 @@ def combustion_range(self) -> int: :return: """ - value = NO_VALUE - if self.is_primary_drive_combustion(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_RANGE].get("value", NO_VALUE) - - elif self.is_secondary_drive_combustion(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_RANGE].get("value", NO_VALUE) - return int(value) + DIESEL_RANGE = "measurements.rangeStatus.value.dieselRange" + GASOLINE_RANGE = "measurements.rangeStatus.value.gasolineRange" + if is_valid_path(self.attrs, DIESEL_RANGE): + return int(find_path(self.attrs, DIESEL_RANGE)) + if is_valid_path(self.attrs, GASOLINE_RANGE): + return int(find_path(self.attrs, GASOLINE_RANGE)) + return -1 @property def combustion_range_last_updated(self) -> datetime | None: """Return combustion engine range last updated.""" - value = None - if self.is_primary_drive_combustion(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_RANGE].get(BACKEND_RECEIVED_TIMESTAMP) - elif self.is_secondary_drive_combustion(): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_RANGE].get(BACKEND_RECEIVED_TIMESTAMP) - return value + return find_path(self.attrs, "measurements.rangeStatus.value.carCapturedTimestamp") @property def is_combustion_range_supported(self) -> bool: @@ -1269,13 +1164,9 @@ def is_combustion_range_supported(self) -> bool: :return: """ - supported = False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if self.is_primary_drive_combustion(): - supported = True - elif self.is_secondary_drive_combustion(): - supported = True - return supported + return is_valid_path(self.attrs, "measurements.rangeStatus.value.dieselRange") or is_valid_path( + self.attrs, "measurements.rangeStatus.value.gasolineRange" + ) @property def combined_range(self) -> int: @@ -1284,18 +1175,12 @@ def combined_range(self) -> int: :return: """ - value = -1 - if P.COMBINED_RANGE in self.attrs.get("StoredVehicleDataResponseParsed"): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.COMBINED_RANGE].get("value", NO_VALUE) - return int(value) + return int(find_path(self.attrs, "measurements.rangeStatus.value.totalRange_km")) @property def combined_range_last_updated(self) -> datetime | None: """Return combined range last updated.""" - value = None - if P.COMBINED_RANGE in self.attrs.get("StoredVehicleDataResponseParsed"): - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.COMBINED_RANGE].get(BACKEND_RECEIVED_TIMESTAMP) - return value + return find_path(self.attrs, "measurements.rangeStatus.value.carCapturedTimestamp") @property def is_combined_range_supported(self) -> bool: @@ -1304,10 +1189,7 @@ def is_combined_range_supported(self) -> bool: :return: """ - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.COMBINED_RANGE in self.attrs.get("StoredVehicleDataResponseParsed"): - return self.is_electric_range_supported and self.is_combustion_range_supported - return False + return is_valid_path(self.attrs, "measurements.rangeStatus.value.totalRange_km") @property def fuel_level(self) -> int: @@ -1316,20 +1198,12 @@ def fuel_level(self) -> int: :return: """ - value = -1 - if P.FUEL_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): - if "value" in self.attrs.get("StoredVehicleDataResponseParsed")[P.FUEL_LEVEL]: - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.FUEL_LEVEL].get("value", 0) - return int(value) + return int(find_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct")) @property def fuel_level_last_updated(self) -> datetime: """Return fuel level last updated.""" - value = datetime.now() - if P.FUEL_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): - if "value" in self.attrs.get("StoredVehicleDataResponseParsed")[P.FUEL_LEVEL]: - value = self.attrs.get("StoredVehicleDataResponseParsed")[P.FUEL_LEVEL].get(BACKEND_RECEIVED_TIMESTAMP) - return value + return find_path(self.attrs, "measurements.fuelLevelStatus.value.carCapturedTimestamp") @property def is_fuel_level_supported(self) -> bool: @@ -1338,10 +1212,7 @@ def is_fuel_level_supported(self) -> bool: :return: """ - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FUEL_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): - return self.is_combustion_range_supported - return False + return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") # Climatisation settings @property @@ -1688,28 +1559,24 @@ def window_closed_left_front(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_WINDOW_CLOSED].get("value", 0)) - return response == CLOSED_STATE + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontLeft": + return "closed" in window["status"] + return False @property def window_closed_left_front_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.FRONT_LEFT_WINDOW_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_window_closed_left_front_supported(self) -> bool: - """Return true if window state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_LEFT_WINDOW_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): - return ( - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_WINDOW_CLOSED].get("value", 0)) - != 0 - ) + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontLeft" and "unsupported" not in window["status"]: + return True return False @property @@ -1719,28 +1586,24 @@ def window_closed_right_front(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_RIGHT_WINDOW_CLOSED].get("value", 0)) - return response == CLOSED_STATE + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontRight": + return "closed" in window["status"] + return False @property def window_closed_right_front_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.FRONT_RIGHT_WINDOW_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_window_closed_right_front_supported(self) -> bool: - """Return true if window state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_RIGHT_WINDOW_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): - return ( - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_RIGHT_WINDOW_CLOSED].get("value", 0)) - != 0 - ) + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontRight" and "unsupported" not in window["status"]: + return True return False @property @@ -1750,28 +1613,24 @@ def window_closed_left_back(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_LEFT_WINDOW_CLOSED].get("value", 0)) - return response == CLOSED_STATE + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearLeft": + return "closed" in window["status"] + return False @property def window_closed_left_back_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.REAR_LEFT_WINDOW_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_window_closed_left_back_supported(self) -> bool: - """Return true if window state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.REAR_LEFT_WINDOW_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): - return ( - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_LEFT_WINDOW_CLOSED].get("value", 0)) - != 0 - ) + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearLeft" and "unsupported" not in window["status"]: + return True return False @property @@ -1781,28 +1640,24 @@ def window_closed_right_back(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_RIGHT_WINDOW_CLOSED].get("value", 0)) - return response == CLOSED_STATE + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearRight": + return "closed" in window["status"] + return False @property def window_closed_right_back_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.REAR_RIGHT_WINDOW_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_window_closed_right_back_supported(self) -> bool: - """Return true if window state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.REAR_RIGHT_WINDOW_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): - return ( - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_RIGHT_WINDOW_CLOSED].get("value", 0)) - != 0 - ) + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearRight" and "unsupported" not in window["status"]: + return True return False @property @@ -1812,25 +1667,51 @@ def sunroof_closed(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.SUNROOF_CLOSED].get("value", 0)) - return response == CLOSED_STATE + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "sunRoof": + return "closed" in window["status"] + return False @property def sunroof_closed_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.SUNROOF_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_sunroof_closed_supported(self) -> bool: - """Return true if sunroof state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.SUNROOF_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): - return int(self.attrs.get("StoredVehicleDataResponseParsed")[P.SUNROOF_CLOSED].get("value", 0)) != 0 + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "sunRoof" and "unsupported" not in window["status"]: + return True + return False + + @property + def roof_cover_closed(self) -> bool: + """ + Return roof cover closed state. + + :return: + """ + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "roofCover": + return "closed" in window["status"] + return False + + @property + def roof_cover_closed_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") + + @property + def is_roof_cover_closed_supported(self) -> bool: + """Return true if supported.""" + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "roofCover" and "unsupported" not in window["status"]: + return True return False # Locks @@ -1961,21 +1842,29 @@ def is_trunk_locked_sensor_supported(self) -> bool: # Doors, hood and trunk @property def hood_closed(self) -> bool: - """Return true if hood is closed.""" - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.HOOD_CLOSED].get("value", 0)) - return response == CLOSED_STATE + """ + Return hood closed state. + + :return: + """ + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "bonnet": + return "closed" in door["status"] + return False @property def hood_closed_last_updated(self) -> datetime: - """Return hood closed last updated.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.HOOD_CLOSED].get("BACKEND_RECEIVED_TIMESTAMP") + """Return attribute last updated timestamp.""" + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_hood_closed_supported(self) -> bool: - """Return true if hood state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.HOOD_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed", {}): - return int(self.attrs.get("StoredVehicleDataResponseParsed")[P.HOOD_CLOSED].get("value", 0)) != 0 + """Return true if supported.""" + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "bonnet" and "unsupported" not in door["status"]: + return True return False @property @@ -1985,21 +1874,23 @@ def door_closed_left_front(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_DOOR_CLOSED].get("value", 0)) - return response == CLOSED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontLeft": + return "closed" in door["status"] + return False @property def door_closed_left_front_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_DOOR_CLOSED].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_door_closed_left_front_supported(self) -> bool: """Return true if supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_LEFT_DOOR_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontLeft" and "unsupported" not in door["status"]: return True return False @@ -2010,21 +1901,23 @@ def door_closed_right_front(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_RIGHT_DOOR_CLOSED].get("value", 0)) - return response == CLOSED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontRight": + return "closed" in door["status"] + return False @property def door_closed_right_front_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_RIGHT_DOOR_CLOSED].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_door_closed_right_front_supported(self) -> bool: """Return true if supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_RIGHT_DOOR_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontRight" and "unsupported" not in door["status"]: return True return False @@ -2035,21 +1928,23 @@ def door_closed_left_back(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_LEFT_DOOR_CLOSED].get("value", 0)) - return response == CLOSED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearLeft": + return "closed" in door["status"] + return False @property def door_closed_left_back_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_LEFT_DOOR_CLOSED].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_door_closed_left_back_supported(self) -> bool: """Return true if supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.REAR_LEFT_DOOR_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearLeft" and "unsupported" not in door["status"]: return True return False @@ -2060,44 +1955,50 @@ def door_closed_right_back(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_RIGHT_DOOR_CLOSED].get("value", 0)) - return response == CLOSED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearRight": + return "closed" in door["status"] + return False @property def door_closed_right_back_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_RIGHT_DOOR_CLOSED].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_door_closed_right_back_supported(self) -> bool: """Return true if supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.REAR_RIGHT_DOOR_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearRight" and "unsupported" not in door["status"]: return True return False @property def trunk_closed(self) -> bool: """ - Return state of trunk closed. + Return trunk closed state. :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_CLOSED].get("value", 0)) - return response == CLOSED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk": + return "closed" in door["status"] + return False @property def trunk_closed_last_updated(self) -> datetime: - """Return trunk closed last updated.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_CLOSED].get("BACKEND_RECEIVED_TIMESTAMP") + """Return attribute last updated timestamp.""" + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_trunk_closed_supported(self) -> bool: - """Return true if trunk closed state is supported.""" - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.TRUNK_CLOSED in self.attrs.get("StoredVehicleDataResponseParsed"): + """Return true if supported.""" + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk" and "unsupported" not in door["status"]: return True return False @@ -2684,17 +2585,13 @@ def is_secondary_drive_electric(self): def is_primary_drive_combustion(self): """Check if primary engine is combustion.""" - return ( - P.PRIMARY_DRIVE in self.attrs.get("StoredVehicleDataResponseParsed", {}) - and self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_DRIVE].get("value", UNSUPPORTED) - in ENGINE_TYPE_COMBUSTION - ) + return find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") in ENGINE_TYPE_COMBUSTION def is_secondary_drive_combustion(self): """Check if secondary engine is combustion.""" return ( - P.SECONDARY_DRIVE in self.attrs.get("StoredVehicleDataResponseParsed", {}) - and self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_DRIVE].get("value", UNSUPPORTED) + # TODO verify + find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") in ENGINE_TYPE_COMBUSTION ) From f2e5f08d216254f071f270c026e172aa4b4dca59 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 28 Nov 2023 15:50:11 +0100 Subject: [PATCH 02/41] revert well-meant renaming of parameter --- volkswagencarnet/vw_vehicle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index f7756edb..6fbe2c89 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -42,10 +42,10 @@ class Vehicle: """Vehicle contains the state of sensors and methods for interacting with the car.""" - def __init__(self, conn, vin): + def __init__(self, conn, url): """Initialize the Vehicle with default values.""" self._connection = conn - self._vin = vin + self._url = url self._homeregion = "https://msg.volkswagen.de" self._discovered = False self._states = {} @@ -663,7 +663,7 @@ def vin(self) -> str: :return: """ - return self._vin + return self._url @property def unique_id(self) -> str: From aa1fc904541673c36add3103aab40633011572e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20=C3=96stlund?= Date: Tue, 28 Nov 2023 16:13:53 +0100 Subject: [PATCH 03/41] adding check on secondary drive --- volkswagencarnet/vw_vehicle.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 6fbe2c89..8e6c2eba 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -2590,11 +2590,13 @@ def is_primary_drive_combustion(self): def is_secondary_drive_combustion(self): """Check if secondary engine is combustion.""" return ( - # TODO verify - find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") - in ENGINE_TYPE_COMBUSTION + # TODO Verify + True if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") + and find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") in ENGINE_TYPE_COMBUSTION + else False ) + def has_combustion_engine(self): """Return true if car has a combustion engine.""" return self.is_primary_drive_combustion() or self.is_secondary_drive_combustion() From 79e3f493e291905f1cdbfefac55ec3f6da9a335e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20=C3=96stlund?= Date: Tue, 28 Nov 2023 16:33:00 +0100 Subject: [PATCH 04/41] fix black --- volkswagencarnet/vw_vehicle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 8e6c2eba..b792e5de 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -2591,8 +2591,10 @@ def is_secondary_drive_combustion(self): """Check if secondary engine is combustion.""" return ( # TODO Verify - True if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") - and find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") in ENGINE_TYPE_COMBUSTION + True + if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") + and find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") + in ENGINE_TYPE_COMBUSTION else False ) From fbd8b5b7b9640658a8274adb50390b321fd4175a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20=C3=96stlund?= Date: Tue, 28 Nov 2023 16:35:38 +0100 Subject: [PATCH 05/41] fix black --- volkswagencarnet/vw_vehicle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index b792e5de..61306fd9 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -2598,7 +2598,6 @@ def is_secondary_drive_combustion(self): else False ) - def has_combustion_engine(self): """Return true if car has a combustion engine.""" return self.is_primary_drive_combustion() or self.is_secondary_drive_combustion() From 3b21d0b5a75f25c5677e9eae5cab91e91be7a6af Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 28 Nov 2023 19:26:45 +0100 Subject: [PATCH 06/41] many properties fixed, some new TODOs :) --- volkswagencarnet/vw_dashboard.py | 17 +++ volkswagencarnet/vw_vehicle.py | 203 +++++++++++-------------------- 2 files changed, 90 insertions(+), 130 deletions(-) diff --git a/volkswagencarnet/vw_dashboard.py b/volkswagencarnet/vw_dashboard.py index 4c15ae15..1d205649 100644 --- a/volkswagencarnet/vw_dashboard.py +++ b/volkswagencarnet/vw_dashboard.py @@ -772,6 +772,20 @@ def create_instruments(): TrunkLock(), RequestUpdate(), WindowHeater(), + BinarySensor( + attr="window_heater_front", + name="Window Heater Front", + device_class=VWDeviceClass.WINDOW, + icon="mdi:car-defrost-front", + reverse_state=True + ), + BinarySensor( + attr="window_heater_back", + name="Window Heater Back", + device_class=VWDeviceClass.WINDOW, + icon="mdi:car-defrost-rear", + reverse_state=True + ), BatteryClimatisation(), ElectricClimatisation(), AuxiliaryClimatisation(), @@ -1052,6 +1066,9 @@ def create_instruments(): BinarySensor( attr="sunroof_closed", name="Sunroof closed", device_class=VWDeviceClass.WINDOW, reverse_state=True ), + BinarySensor( + attr="roof_cover_closed", name="Roof cover closed", device_class=VWDeviceClass.WINDOW, reverse_state=True + ), BinarySensor( attr="windows_closed", name="Windows closed", device_class=VWDeviceClass.WINDOW, reverse_state=True ), diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 61306fd9..a317590e 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -28,10 +28,8 @@ _LOGGER = logging.getLogger(__name__) -# TODO find out -ENGINE_TYPE_ELECTRIC = "3" +ENGINE_TYPE_ELECTRIC = "electric" ENGINE_TYPE_DIESEL = "diesel" -# TODO verify ENGINE_TYPE_GASOLINE = "gasoline" ENGINE_TYPE_COMBUSTION = [ENGINE_TYPE_DIESEL, ENGINE_TYPE_GASOLINE] @@ -1218,50 +1216,38 @@ def is_fuel_level_supported(self) -> bool: @property def climatisation_target_temperature(self) -> float | None: """Return the target temperature from climater.""" - value = self.attrs.get("climater").get("settings").get("targetTemperature").get("content") - if value: - reply = float((value / 10) - 273) - self._climatisation_target_temperature = reply - return reply - else: - return None + # TODO should we handle Fahrenheit?? + return int(find_path(self.attrs, "climatisation.climatisationSettings.value.targetTemperature_C")) @property def climatisation_target_temperature_last_updated(self) -> datetime: """Return the target temperature from climater last updated.""" - return self.attrs.get("climater").get("settings").get("targetTemperature").get(BACKEND_RECEIVED_TIMESTAMP) + return find_path(self.attrs, "climatisation.climatisationSettings.value.carCapturedTimestamp") @property def is_climatisation_target_temperature_supported(self) -> bool: """Return true if climatisation target temperature is supported.""" - if self.attrs.get("climater", False): - if "settings" in self.attrs.get("climater", {}): - if "targetTemperature" in self.attrs.get("climater", {})["settings"]: - return True - return False + return is_valid_path(self.attrs, "climatisation.climatisationSettings.value.targetTemperature_C") @property def climatisation_without_external_power(self): """Return state of climatisation from battery power.""" - return self.attrs.get("climater").get("settings").get("climatisationWithoutHVpower").get("content", False) + return find_path(self.attrs, "climatisation.climatisationSettings.value.climatisationWithoutExternalPower") @property def climatisation_without_external_power_last_updated(self) -> datetime: """Return state of climatisation from battery power last updated.""" - return self.attrs.get("climater").get("settings").get("climatisationWithoutHVpower").get("timestamp") + return find_path(self.attrs, "climatisation.climatisationSettings.value.carCapturedTimestamp") @property def is_climatisation_without_external_power_supported(self) -> bool: """Return true if climatisation on battery power is supported.""" - if self.attrs.get("climater", False): - if "settings" in self.attrs.get("climater", {}): - if "climatisationWithoutHVpower" in self.attrs.get("climater", {})["settings"]: - return True - return False + return is_valid_path(self.attrs, "climatisation.climatisationSettings.value.climatisationWithoutExternalPower") @property def outside_temperature(self) -> float | bool: # FIXME should probably be Optional[float] instead """Return outside temperature.""" + # TODO not found yet response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.OUTSIDE_TEMPERATURE].get("value", None)) if response is not None: return round(float((response / 10) - 273.15), 1) @@ -1271,11 +1257,13 @@ def outside_temperature(self) -> float | bool: # FIXME should probably be Optio @property def outside_temperature_last_updated(self) -> datetime: """Return outside temperature last updated.""" + # TODO not found yet return self.attrs.get("StoredVehicleDataResponseParsed")[P.OUTSIDE_TEMPERATURE].get(BACKEND_RECEIVED_TIMESTAMP) @property def is_outside_temperature_supported(self) -> bool: """Return true if outside temp is supported.""" + # TODO not found yet if self.attrs.get("StoredVehicleDataResponseParsed", False): if P.OUTSIDE_TEMPERATURE in self.attrs.get("StoredVehicleDataResponseParsed"): if "value" in self.attrs.get("StoredVehicleDataResponseParsed")[P.OUTSIDE_TEMPERATURE]: @@ -1286,6 +1274,7 @@ def is_outside_temperature_supported(self) -> bool: @property def electric_climatisation(self) -> bool: """Return status of climatisation.""" + # TODO not found yet climatisation_type = ( self.attrs.get("climater", {}).get("settings", {}).get("heaterSource", {}).get("content", "") ) @@ -1301,6 +1290,7 @@ def electric_climatisation(self) -> bool: @property def electric_climatisation_last_updated(self) -> datetime: """Return status of climatisation last updated.""" + # TODO not found yet return ( self.attrs.get("climater", {}) .get("status", {}) @@ -1312,131 +1302,93 @@ def electric_climatisation_last_updated(self) -> datetime: @property def is_electric_climatisation_supported(self) -> bool: """Return true if vehicle has climater.""" + # TODO not found yet return self.is_climatisation_supported @property def auxiliary_climatisation(self) -> bool: """Return status of auxiliary climatisation.""" - climatisation_type = ( - self.attrs.get("climater", {}).get("settings", {}).get("heaterSource", {}).get("content", "") - ) - status = ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("climatisationStatusData", {}) - .get("climatisationState", {}) - .get("content", "") - ) - if status in ["heating", "heatingAuxiliary", "on"] and climatisation_type == "auxiliary": - return True - elif status in ["heatingAuxiliary"] and climatisation_type == "electric": + climatisation_state = find_path(self.attrs, "climatisation.climatisationStatus.value.climatisationState") + if climatisation_state in ["heating", "heatingAuxiliary", "on"]: return True - else: - return False @property def auxiliary_climatisation_last_updated(self) -> datetime: """Return status of auxiliary climatisation last updated.""" - return ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("climatisationStatusData", {}) - .get("climatisationState", {}) - .get(BACKEND_RECEIVED_TIMESTAMP) - ) + return find_path(self.attrs, "climatisation.climatisationStatus.value.carCapturedTimestamp") @property def is_auxiliary_climatisation_supported(self) -> bool: """Return true if vehicle has auxiliary climatisation.""" - if self._services.get("rclima_v1", False): - functions = self._services.get("rclima_v1", {}).get("operations", []) - if "P_START_CLIMA_AU" in functions: - return True - return False + return is_valid_path(self.attrs, "climatisation.climatisationStatus.value.climatisationState") @property def is_climatisation_supported(self) -> bool: """Return true if climatisation has State.""" - response = ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("climatisationStatusData", {}) - .get("climatisationState", {}) - .get("content", "") - ) - return response != "" + return is_valid_path(self.attrs, "climatisation.climatisationStatus.value.climatisationState") @property def is_climatisation_supported_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("climatisationStatusData", {}) - .get("climatisationState", {}) - .get(BACKEND_RECEIVED_TIMESTAMP) - ) + return find_path(self.attrs, "climatisation.climatisationStatus.value.carCapturedTimestamp") @property - def window_heater(self) -> bool: - """Return status of window heater.""" + def window_heater_front(self) -> bool: + """Return status of front window heater.""" ret = False - status_front = ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("windowHeatingStatusData", {}) - .get("windowHeatingStateFront", {}) - .get("content", "") - ) - if status_front == "on": - ret = True + window_heating_status = find_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") + for window_heating_state in window_heating_status: + if window_heating_state["windowLocation"] == "front": + return window_heating_state["windowHeatingState"] == "on" - status_rear = ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("windowHeatingStatusData", {}) - .get("windowHeatingStateRear", {}) - .get("content", "") - ) - if status_rear == "on": - ret = True - return ret + return False @property - def window_heater_last_updated(self) -> datetime: - """Return window heater last updated.""" - front = ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("windowHeatingStatusData", {}) - .get("windowHeatingStateFront", {}) - .get(BACKEND_RECEIVED_TIMESTAMP) - ) - if front is not None: - return front - - return ( - self.attrs.get("climater", {}) - .get("status", {}) - .get("windowHeatingStatusData", {}) - .get("windowHeatingStateRear", {}) - .get(BACKEND_RECEIVED_TIMESTAMP) - ) + def window_heater_front_last_updated(self) -> datetime: + """Return front window heater last updated.""" + return find_path(self.attrs, "climatisation.windowHeatingStatus.value.carCapturedTimestamp") @property - def is_window_heater_supported(self) -> bool: + def is_window_heater_front_supported(self) -> bool: """Return true if vehicle has heater.""" - if self.is_electric_climatisation_supported: - if self.attrs.get("climater", {}).get("status", {}).get("windowHeatingStatusData", {}).get( - "windowHeatingStateFront", {} - ).get("content", "") in ["on", "off"]: - return True - if self.attrs.get("climater", {}).get("status", {}).get("windowHeatingStatusData", {}).get( - "windowHeatingStateRear", {} - ).get("content", "") in ["on", "off"]: - return True + return is_valid_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") + + @property + def window_heater_back(self) -> bool: + """Return status of rear window heater.""" + ret = False + window_heating_status = find_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") + for window_heating_state in window_heating_status: + if window_heating_state["windowLocation"] == "rear": + return window_heating_state["windowHeatingState"] == "on" + return False + @property + def window_heater_rear_last_updated(self) -> datetime: + """Return front window heater last updated.""" + return find_path(self.attrs, "climatisation.windowHeatingStatus.value.carCapturedTimestamp") + + @property + def is_window_heater_rear_supported(self) -> bool: + """Return true if vehicle has heater.""" + return is_valid_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") + + @property + def window_heater(self) -> bool: + """Return status of window heater.""" + return self.window_heater_front or self.window_heater_back + + @property + def window_heater_last_updated(self) -> datetime: + """Return front window heater last updated.""" + return self.window_heater_front_last_updated + + @property + def is_window_supported(self) -> bool: + """Return true if vehicle has heater.""" + return self.is_window_heater_front_supported + # Parking heater, "legacy" auxiliary climatisation @property def pheater_duration(self) -> int: @@ -2569,18 +2521,14 @@ def serialize(obj): def is_primary_drive_electric(self): """Check if primary engine is electric.""" - return ( - P.PRIMARY_DRIVE in self.attrs.get("StoredVehicleDataResponseParsed", {}) - and self.attrs.get("StoredVehicleDataResponseParsed")[P.PRIMARY_DRIVE].get("value", UNSUPPORTED) - == ENGINE_TYPE_ELECTRIC - ) + return find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") == ENGINE_TYPE_ELECTRIC + def is_secondary_drive_electric(self): """Check if secondary engine is electric.""" return ( - P.SECONDARY_DRIVE in self.attrs.get("StoredVehicleDataResponseParsed", {}) - and self.attrs.get("StoredVehicleDataResponseParsed")[P.SECONDARY_DRIVE].get("value", UNSUPPORTED) - == ENGINE_TYPE_ELECTRIC + is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") and + find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") == ENGINE_TYPE_ELECTRIC ) def is_primary_drive_combustion(self): @@ -2589,13 +2537,8 @@ def is_primary_drive_combustion(self): def is_secondary_drive_combustion(self): """Check if secondary engine is combustion.""" - return ( - # TODO Verify - True - if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") - and find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") - in ENGINE_TYPE_COMBUSTION - else False + return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") and ( + find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") in ENGINE_TYPE_COMBUSTION ) def has_combustion_engine(self): From 9699332a3a2436255fe2913a6457ab3b075842aa Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 28 Nov 2023 19:32:27 +0100 Subject: [PATCH 07/41] fixed unused variables --- volkswagencarnet/vw_vehicle.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index a317590e..39c7841b 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1335,7 +1335,6 @@ def is_climatisation_supported_last_updated(self) -> datetime: @property def window_heater_front(self) -> bool: """Return status of front window heater.""" - ret = False window_heating_status = find_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") for window_heating_state in window_heating_status: if window_heating_state["windowLocation"] == "front": @@ -1356,7 +1355,6 @@ def is_window_heater_front_supported(self) -> bool: @property def window_heater_back(self) -> bool: """Return status of rear window heater.""" - ret = False window_heating_status = find_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") for window_heating_state in window_heating_status: if window_heating_state["windowLocation"] == "rear": From d94b54591a5adae92bd748d75b02df05b9994c08 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 28 Nov 2023 19:51:36 +0100 Subject: [PATCH 08/41] some cleanup of comments --- volkswagencarnet/vw_connection.py | 25 ---------------------- volkswagencarnet/vw_vehicle.py | 35 ++++++++++--------------------- 2 files changed, 11 insertions(+), 49 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 97356e7e..86764b31 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -106,14 +106,8 @@ async def doLogin(self, tries: int = 1): self._session_tokens["identity"] = self._session_tokens["Legacy"].copy() self._session_logged_in = True - # Get VW-Group API tokens - # if not await self._getAPITokens(): - # self._session_logged_in = False - # return False - # Get list of vehicles from account _LOGGER.debug("Fetching vehicles associated with account") - # await self.set_token("vwg") self._session_headers.pop("Content-Type", None) loaded_vehicles = await self.get(url=f"{BASE_API}/vehicle/v2/vehicles") # Add Vehicle class object for all VIN-numbers from account @@ -127,7 +121,6 @@ async def doLogin(self, tries: int = 1): return False # Update all vehicles data before returning - # await self.set_token("vwg") await self.update() return True @@ -586,7 +579,6 @@ async def getHomeRegion(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") # TODO: handle multiple home regions! (no examples available currently) return True response = await self.get( @@ -609,7 +601,6 @@ async def getOperationList(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", "") if response.get("capabilities", False): data = response.get("capabilities", {}) @@ -689,7 +680,6 @@ async def getRealCarData(self, vin): subject = jwt.decode(atoken, options={"verify_signature": False}, algorithms=JWT_ALGORITHMS).get( "sub", None ) - # await self.set_token("identity") self._session_headers["Accept"] = "application/json" response = await self.get(f"https://customer-profile.vwgroup.io/v1/customers/{subject}/realCarData") if response.get("realCars", {}): @@ -712,7 +702,6 @@ async def getCarportData(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") self._session_headers["Accept"] = ( "application/vnd.vwg.mbb.vehicleDataDetail_v2_1_0+json," " application/vnd.vwg.mbb.genericError_v1_0_2+json" @@ -736,7 +725,6 @@ async def getCarportData(self, vin): async def getVehicleStatusData(self, vin): """Get stored vehicle data response.""" try: - # await self.set_token("vwg") response = await self.get(f"fs-car/bs/vsr/v1/{BRAND}/{self._session_country}/vehicles/$vin/status", vin=vin) if ( response.get("StoredVehicleDataResponse", {}) @@ -768,7 +756,6 @@ async def getTripStatistics(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get( f"fs-car/bs/tripstatistics/v1/{BRAND}/{self._session_country}/vehicles/$vin/tripdata/shortTerm?newest", vin=vin, @@ -789,7 +776,6 @@ async def getPosition(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get( f"fs-car/bs/cf/v1/{BRAND}/{self._session_country}/vehicles/$vin/position", vin=vin ) @@ -814,7 +800,6 @@ async def getTimers(self, vin) -> TimerData | None: if not await self.validate_tokens: return None try: - # await self.set_token("vwg") response = await self.get( f"fs-car/bs/departuretimer/v1/{BRAND}/{self._session_country}/vehicles/$vin/timer", vin=vin ) @@ -834,7 +819,6 @@ async def getClimater(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get( f"fs-car/bs/climatisation/v1/{BRAND}/{self._session_country}/vehicles/$vin/climater", vin=vin ) @@ -854,7 +838,6 @@ async def getCharger(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get( f"fs-car/bs/batterycharge/v1/{BRAND}/{self._session_country}/vehicles/$vin/charger", vin=vin ) @@ -874,7 +857,6 @@ async def getPreHeater(self, vin): if not await self.validate_tokens: return False try: - # await self.set_token("vwg") response = await self.get(f"fs-car/bs/rs/v1/{BRAND}/{self._session_country}/vehicles/$vin/status", vin=vin) if response.get("statusResponse", {}): data = {"heating": response.get("statusResponse", {})} @@ -899,7 +881,6 @@ async def get_request_status(self, vin, sectionId, requestId): if not await self.doLogin(): _LOGGER.warning(f"Login for {BRAND} account failed!") raise Exception(f"Login for {BRAND} account failed") - # await self.set_token("vwg") if sectionId == "climatisation": url = ( f"fs-car/bs/$sectionId/v1/{BRAND}/{self._session_country}/vehicles/$vin/climater/actions/$requestId" @@ -1024,7 +1005,6 @@ async def dataCall(self, query, vin="", **data): async def setRefresh(self, vin): """Force vehicle data update.""" try: - # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/vsr/v1/{BRAND}/{self._session_country}/vehicles/$vin/requests", vin, data=None ) @@ -1047,7 +1027,6 @@ async def setRefresh(self, vin): async def setCharger(self, vin, data) -> dict[str, str | int | None]: """Start/Stop charger.""" try: - # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/batterycharge/v1/{BRAND}/{self._session_country}/vehicles/$vin/charger/actions", vin, @@ -1072,7 +1051,6 @@ async def setCharger(self, vin, data) -> dict[str, str | int | None]: async def setClimater(self, vin, data, spin): """Execute climatisation actions.""" try: - # await self.set_token("vwg") # Only get security token if auxiliary heater is to be started if data.get("action", {}).get("settings", {}).get("heaterSource", None) == "auxiliary": self._session_headers["X-securityToken"] = await self.get_sec_token(vin=vin, spin=spin, action="rclima") @@ -1103,7 +1081,6 @@ async def setPreHeater(self, vin, data, spin): """Petrol/diesel parking heater actions.""" content_type = None try: - # await self.set_token("vwg") if "Content-Type" in self._session_headers: content_type = self._session_headers["Content-Type"] else: @@ -1163,7 +1140,6 @@ async def setChargeMinLevel(self, vin: str, limit: int): async def _setDepartureTimer(self, vin, data: TimersAndProfiles, action: str): """Set schedules.""" try: - # await self.set_token("vwg") response = await self.dataCall( f"fs-car/bs/departuretimer/v1/{BRAND}/{self._session_country}/vehicles/$vin/timer/actions", vin=vin, @@ -1197,7 +1173,6 @@ async def setLock(self, vin, data, spin): """Remote lock and unlock actions.""" content_type = None try: - # await self.set_token("vwg") # Prepare data, headers and fetch security token if "Content-Type" in self._session_headers: content_type = self._session_headers["Content-Type"] diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 39c7841b..b729bc2c 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -63,19 +63,20 @@ def __init__(self, conn, url): # API Endpoints that might be enabled for car (that we support) self._services: dict[str, dict[str, Any]] = { - # "rheating_v1": {"active": False}, # TODO: equivalent in new API unknown - # "rclima_v1": {"active": False}, # TODO: equivalent in new API unknown + # TODO needs a complete rework... + # "rheating_v1": {"active": False}, + # "rclima_v1": {"active": False}, "access": {"active": False}, "tripStatistics": {"active": False}, "measurements": {"active": False}, - # "statusreport_v1": {"active": False}, # TODO: equivalent in new API unknown, potentially "state"? - # "rbatterycharge_v1": {"active": False}, # TODO: equivalent in new API unknown + # "statusreport_v1": {"active": False}, + # "rbatterycharge_v1": {"active": False}, "honkAndFlash": {"active": False}, - # "carfinder_v1": {"active": False}, # TODO: equivalent in new API unknown - # "timerprogramming_v1": {"active": False}, # TODO: equivalent in new API unknown - # "jobs_v1": {"active": False}, # TODO: equivalent in new API unknown - # "owner_v1": {"active": False}, # TODO: equivalent in new API unknown - # vehicles_v1_cai, services_v1, vehicletelemetry_v1 # TODO: equivalent in new API unknown + # "carfinder_v1": {"active": False}, + # "timerprogramming_v1": {"active": False}, + # "jobs_v1": {"active": False}, + # "owner_v1": {"active": False}, + # vehicles_v1_cai, services_v1, vehicletelemetry_v1 } def _in_progress(self, topic: str, unknown_offset: int = 0) -> bool: @@ -119,19 +120,10 @@ async def _handle_response(self, response, topic: str, error_msg: str | None = N # Init and update vehicle data async def discover(self): """Discover vehicle and initial data.""" - # homeregion = await self._connection.getHomeRegion(self.vin) - # _LOGGER.debug(f"Get homeregion for VIN {self.vin}") - # if homeregion: - # self._homeregion = homeregion - - # await asyncio.gather(self.get_carportdata(), self.get_realcardata(), return_exceptions=True) - # _LOGGER.info(f'Vehicle {self.vin} added. Homeregion is "{self._homeregion}"') _LOGGER.debug("Attempting discovery of supported API endpoints for vehicle.") operation_list = await self._connection.getOperationList(self.vin) if operation_list: - # service_info = operation_list["serviceInfo"] - # Iterate over all endpoints in ServiceInfo list for service_id in operation_list.keys(): try: if service_id in self._services.keys(): @@ -1485,12 +1477,7 @@ def windows_closed(self) -> bool: @property def windows_closed_last_updated(self) -> datetime: """Return timestamp for windows state last updated.""" - return ( - self.attrs.get("StoredVehicleDataResponseParsed", {}) - .get("StoredVehicleDataResponseParsed", {}) - .get(P.FRONT_LEFT_WINDOW_CLOSED, {}) - .get("BACKEND_RECEIVED_TIMESTAMP") - ) + return self.window_closed_left_front_last_updated @property def is_windows_closed_supported(self) -> bool: From 13d5df144b77075004436e636915a18e97532f77 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Wed, 29 Nov 2023 09:23:34 +0100 Subject: [PATCH 09/41] added alternative path for engine types --- volkswagencarnet/vw_vehicle.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index b729bc2c..eeb72c45 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -2518,13 +2518,25 @@ def is_secondary_drive_electric(self): def is_primary_drive_combustion(self): """Check if primary engine is combustion.""" - return find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") in ENGINE_TYPE_COMBUSTION + engine_type = "" + if is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.type"): + engine_type = find_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.type") + + if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType"): + engine_type = find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") + + return engine_type in ENGINE_TYPE_COMBUSTION def is_secondary_drive_combustion(self): """Check if secondary engine is combustion.""" - return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") and ( - find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") in ENGINE_TYPE_COMBUSTION - ) + engine_type = "" + if is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.secondaryEngine.type"): + engine_type = find_path(self.attrs, "fuelStatus.rangeStatus.value.secondaryEngine.type") + + if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType"): + engine_type = find_path(self.attrs, "measurements.fuelLevelStatus.value.secondaryEngineType") + + return engine_type in ENGINE_TYPE_COMBUSTION def has_combustion_engine(self): """Return true if car has a combustion engine.""" From 72c8e261404ff0df1cb2338ed46ec8a99562bcf7 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Wed, 29 Nov 2023 09:51:07 +0100 Subject: [PATCH 10/41] add parking position --- volkswagencarnet/vw_connection.py | 30 +++--------------------------- volkswagencarnet/vw_vehicle.py | 24 +++++++++--------------- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 86764b31..79e6c37d 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -536,6 +536,7 @@ async def post(self, url, vin="", tries=0, **data): # Construct URL from request, home region and variables def _make_url(self, ref, vin=""): + # TODO after verifying that we don't need home region handling anymore, this method should be completely removed return ref replacedUrl = re.sub("\\$vin", vin, ref) if "://" in replacedUrl: @@ -663,8 +664,8 @@ async def getParkingPosition(self, vin): if "data" in response: return {"parkingposition": response["data"]} - - _LOGGER.warning(f"Could not fetch parkingposition for vin {vin}") + else: + return {"parkingposition": {}} except Exception as error: _LOGGER.warning(f"Could not fetch parkingposition, error: {error}") @@ -697,31 +698,6 @@ async def getRealCarData(self, vin): _LOGGER.warning(f"Could not fetch realCarData, error: {error}") return False - async def getCarportData(self, vin): - """Get carport data for vehicle, model, model year etc.""" - if not await self.validate_tokens: - return False - try: - self._session_headers["Accept"] = ( - "application/vnd.vwg.mbb.vehicleDataDetail_v2_1_0+json," - " application/vnd.vwg.mbb.genericError_v1_0_2+json" - ) - response = await self.get( - f"fs-car/vehicleMgmt/vehicledata/v2/{BRAND}/{self._session_country}/vehicles/$vin", vin=vin - ) - self._session_headers["Accept"] = "application/json" - - if response.get("vehicleDataDetail", {}).get("carportData", {}): - data = {"carportData": response.get("vehicleDataDetail", {}).get("carportData", {})} - return data - elif response.get("status_code", {}): - _LOGGER.warning(f'Could not fetch carportdata, HTTP status code: {response.get("status_code")}') - else: - _LOGGER.info("Unhandled error while trying to fetch carport data") - except Exception as error: - _LOGGER.warning(f"Could not fetch carportData, error: {error}") - return False - async def getVehicleStatusData(self, vin): """Get stored vehicle data response.""" try: diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index eeb72c45..dbb0bfc7 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -213,12 +213,6 @@ async def get_realcardata(self): if data: self._states.update(data) - async def get_carportdata(self): - """Fetch carport data.""" - data = await self._connection.getCarportData(self.vin) - if data: - self._states.update(data) - async def get_preheater(self): """Fetch pre-heater data if function is enabled.""" if self._services.get("rheating_v1", {}).get("active", False): @@ -1049,10 +1043,9 @@ def position(self) -> dict[str, str | float | None]: if self.vehicle_moving: output = {"lat": None, "lng": None, "timestamp": None} else: - pos_obj = self.attrs.get("findCarResponse", {}) - lat = int(pos_obj.get("Position").get("carCoordinate").get("latitude")) / 1000000 - lng = int(pos_obj.get("Position").get("carCoordinate").get("longitude")) / 1000000 - parking_time = pos_obj.get("parkingTimeUTC") + lat = float(find_path(self.attrs, "parkingposition.lat")) + lng = float(find_path(self.attrs, "parkingposition.lon")) + parking_time = find_path(self.attrs, "parkingposition.carCapturedTimestamp") output = {"lat": lat, "lng": lng, "timestamp": parking_time} except Exception: output = { @@ -1064,22 +1057,23 @@ def position(self) -> dict[str, str | float | None]: @property def position_last_updated(self) -> datetime: """Return position last updated.""" - return self.attrs.get("findCarResponse", {}).get("Position", {}).get("timestampTssReceived") + return find_path(self.attrs, "parkingposition.carCapturedTimestamp") @property def is_position_supported(self) -> bool: - """Return true if carfinder_v1 service is active.""" - return self._services.get("carfinder_v1", {}).get("active", False) or self.attrs.get("isMoving", False) + """Return true if position is available.""" + return is_valid_path(self.attrs, "parkingposition.carCapturedTimestamp") @property def vehicle_moving(self) -> bool: """Return true if vehicle is moving.""" - return self.attrs.get("isMoving", False) + # there is not "isMoving" property anymore in VW's API, so we just take the absence of position data as the indicator + return not is_valid_path(self.attrs, "parkingposition.lat") @property def vehicle_moving_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("findCarResponse", {}).get("Position", {}).get("timestampTssReceived") + return find_path(self.attrs, "parkingposition.carCapturedTimestamp") @property def is_vehicle_moving_supported(self) -> bool: From 8c00de0424132659417127f04e1ed3a3f5a273c4 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Wed, 29 Nov 2023 10:37:10 +0100 Subject: [PATCH 11/41] added last trip and locked information --- volkswagencarnet/vw_connection.py | 15 ++ volkswagencarnet/vw_vehicle.py | 296 ++++++++++++++++-------------- 2 files changed, 171 insertions(+), 140 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 79e6c37d..33558ab0 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -671,6 +671,21 @@ async def getParkingPosition(self, vin): _LOGGER.warning(f"Could not fetch parkingposition, error: {error}") return False + async def getTripLast(self, vin): + """Get car information like VIN, nickname, etc.""" + if not await self.validate_tokens: + return False + try: + response = await self.get(f"{BASE_API}/vehicle/v1/trips/{vin}/shortterm/last", "") + if "data" in response: + return {"trip_last": response["data"]} + else: + _LOGGER.warning(f"Could not fetch last trip data, server response: {response}") + + except Exception as error: + _LOGGER.warning(f"Could not fetch last trip data, error: {error}") + return False + async def getRealCarData(self, vin): """Get car information from customer profile, VIN, nickname, etc.""" if not await self.validate_tokens: diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index dbb0bfc7..8e27e8bb 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -64,14 +64,15 @@ def __init__(self, conn, url): # API Endpoints that might be enabled for car (that we support) self._services: dict[str, dict[str, Any]] = { # TODO needs a complete rework... - # "rheating_v1": {"active": False}, - # "rclima_v1": {"active": False}, "access": {"active": False}, "tripStatistics": {"active": False}, "measurements": {"active": False}, + "honkAndFlash": {"active": False}, + "parkingPosition": {"active" : False} + # "rheating_v1": {"active": False}, + # "rclima_v1": {"active": False}, # "statusreport_v1": {"active": False}, # "rbatterycharge_v1": {"active": False}, - "honkAndFlash": {"active": False}, # "carfinder_v1": {"active": False}, # "timerprogramming_v1": {"active": False}, # "jobs_v1": {"active": False}, @@ -175,7 +176,8 @@ async def update(self): ] ), self.get_vehicle(), - self.get_parkingposition() + self.get_parkingposition(), + self.get_trip_last() # self.get_preheater(), # self.get_climater(), # self.get_trip_statistic(), @@ -202,10 +204,18 @@ async def get_vehicle(self): self._states.update(data) async def get_parkingposition(self): - """Fetch parking position.""" - data = await self._connection.getParkingPosition(self.vin) - if data: - self._states.update(data) + """Fetch parking position if supported.""" + if self._services.get("parkingPosition", {}).get("active", False): + data = await self._connection.getParkingPosition(self.vin) + if data: + self._states.update(data) + + async def get_trip_last(self): + """Fetch last trip statistics if supported.""" + if self._services.get("tripStatistics", {}).get("active", False): + data = await self._connection.getTripLast(self.vin) + if data: + self._states.update(data) async def get_realcardata(self): """Fetch realcardata.""" @@ -1504,10 +1514,11 @@ def window_closed_left_front_last_updated(self) -> datetime: @property def is_window_closed_left_front_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "frontLeft" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.windows"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontLeft" and "unsupported" not in window["status"]: + return True return False @property @@ -1531,10 +1542,11 @@ def window_closed_right_front_last_updated(self) -> datetime: @property def is_window_closed_right_front_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "frontRight" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.windows"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "frontRight" and "unsupported" not in window["status"]: + return True return False @property @@ -1558,10 +1570,11 @@ def window_closed_left_back_last_updated(self) -> datetime: @property def is_window_closed_left_back_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "rearLeft" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.windows"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearLeft" and "unsupported" not in window["status"]: + return True return False @property @@ -1585,10 +1598,11 @@ def window_closed_right_back_last_updated(self) -> datetime: @property def is_window_closed_right_back_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "rearRight" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.windows"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "rearRight" and "unsupported" not in window["status"]: + return True return False @property @@ -1612,10 +1626,11 @@ def sunroof_closed_last_updated(self) -> datetime: @property def is_sunroof_closed_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "sunRoof" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.windows"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "sunRoof" and "unsupported" not in window["status"]: + return True return False @property @@ -1639,10 +1654,11 @@ def roof_cover_closed_last_updated(self) -> datetime: @property def is_roof_cover_closed_supported(self) -> bool: """Return true if supported.""" - windows = find_path(self.attrs, "access.accessStatus.value.windows") - for window in windows: - if window["name"] == "roofCover" and "unsupported" not in window["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + windows = find_path(self.attrs, "access.accessStatus.value.windows") + for window in windows: + if window["name"] == "roofCover" and "unsupported" not in window["status"]: + return True return False # Locks @@ -1658,29 +1674,17 @@ def door_locked(self) -> bool: :return: """ - return all( - s == LOCKED_STATE - for s in [ - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_DOOR_LOCK].get("value", 0)), - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.REAR_LEFT_DOOR_LOCK].get("value", 0)), - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_RIGHT_DOOR_LOCK].get("value", 0)), - int(self.attrs.get("StoredVehicleDataResponseParsed")[P.READ_RIGHT_DOOR_LOCK].get("value", 0)), - ] - ) + return find_path(self.attrs, "access.accessStatus.value.doorLockStatus") == "locked" @property def door_locked_last_updated(self) -> datetime: """Return door lock last updated.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_DOOR_LOCK].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def door_locked_sensor_last_updated(self) -> datetime: """Return door lock last updated.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.FRONT_LEFT_DOOR_LOCK].get( - "BACKEND_RECEIVED_TIMESTAMP" - ) + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_door_locked_supported(self) -> bool: @@ -1689,13 +1693,7 @@ def is_door_locked_supported(self) -> bool: :return: """ - # First check that the service is actually enabled - if not self._services.get("rlu_v1", {}).get("active", False): - return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_LEFT_DOOR_LOCK in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - return False + return is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus") @property def is_door_locked_sensor_supported(self) -> bool: @@ -1704,13 +1702,7 @@ def is_door_locked_sensor_supported(self) -> bool: :return: """ - # Use real lock if the service is actually enabled - if self._services.get("rlu_v1", {}).get("active", False): - return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.FRONT_LEFT_DOOR_LOCK in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - return False + return is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus") @property def trunk_locked(self) -> bool: @@ -1719,13 +1711,16 @@ def trunk_locked(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_LOCK].get("value", 0)) - return response == LOCKED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk": + return "locked" in door["status"] + return False @property def trunk_locked_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_LOCK].get("BACKEND_RECEIVED_TIMESTAMP") + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_trunk_locked_supported(self) -> bool: @@ -1734,11 +1729,11 @@ def is_trunk_locked_supported(self) -> bool: :return: """ - if not self._services.get("rlu_v1", {}).get("active", False): - return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.TRUNK_LOCK in self.attrs.get("StoredVehicleDataResponseParsed"): - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk" and "unsupported" not in door["status"]: + return True return False @property @@ -1748,13 +1743,16 @@ def trunk_locked_sensor(self) -> bool: :return: """ - response = int(self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_LOCK].get("value", 0)) - return response == LOCKED_STATE + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk": + return "locked" in door["status"] + return False @property def trunk_locked_sensor_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("StoredVehicleDataResponseParsed")[P.TRUNK_LOCK].get("BACKEND_RECEIVED_TIMESTAMP") + return find_path(self.attrs, "access.accessStatus.value.carCapturedTimestamp") @property def is_trunk_locked_sensor_supported(self) -> bool: @@ -1763,11 +1761,11 @@ def is_trunk_locked_sensor_supported(self) -> bool: :return: """ - if self._services.get("rlu_v1", {}).get("active", False): - return False - if self.attrs.get("StoredVehicleDataResponseParsed", False): - if P.TRUNK_LOCK in self.attrs.get("StoredVehicleDataResponseParsed"): - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk" and "unsupported" not in door["status"]: + return True return False # Doors, hood and trunk @@ -1792,10 +1790,11 @@ def hood_closed_last_updated(self) -> datetime: @property def is_hood_closed_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "bonnet" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "bonnet" and "unsupported" not in door["status"]: + return True return False @property @@ -1819,10 +1818,11 @@ def door_closed_left_front_last_updated(self) -> datetime: @property def is_door_closed_left_front_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "frontLeft" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontLeft" and "unsupported" not in door["status"]: + return True return False @property @@ -1846,10 +1846,11 @@ def door_closed_right_front_last_updated(self) -> datetime: @property def is_door_closed_right_front_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "frontRight" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "frontRight" and "unsupported" not in door["status"]: + return True return False @property @@ -1873,10 +1874,11 @@ def door_closed_left_back_last_updated(self) -> datetime: @property def is_door_closed_left_back_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "rearLeft" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearLeft" and "unsupported" not in door["status"]: + return True return False @property @@ -1900,10 +1902,11 @@ def door_closed_right_back_last_updated(self) -> datetime: @property def is_door_closed_right_back_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "rearRight" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "rearRight" and "unsupported" not in door["status"]: + return True return False @property @@ -1927,10 +1930,11 @@ def trunk_closed_last_updated(self) -> datetime: @property def is_trunk_closed_supported(self) -> bool: """Return true if supported.""" - doors = find_path(self.attrs, "access.accessStatus.value.doors") - for door in doors: - if door["name"] == "trunk" and "unsupported" not in door["status"]: - return True + if is_valid_path(self.attrs, "access.accessStatus.value.doors"): + doors = find_path(self.attrs, "access.accessStatus.value.doors") + for door in doors: + if door["name"] == "trunk" and "unsupported" not in door["status"]: + return True return False # Departure timers @@ -2088,7 +2092,7 @@ def trip_last_entry(self): :return: """ - return self.attrs.get("tripstatistics", {}) + return self.attrs.get("trip_last", {}) @property def trip_last_average_speed(self): @@ -2097,12 +2101,12 @@ def trip_last_average_speed(self): :return: """ - return self.trip_last_entry.get("averageSpeed") + return find_path(self.attrs, "trip_last.averageSpeed_kmph") @property def trip_last_average_speed_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_average_speed_supported(self) -> bool: @@ -2111,8 +2115,10 @@ def is_trip_last_average_speed_supported(self) -> bool: :return: """ - response = self.trip_last_entry - return response and type(response.get("averageSpeed", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.averageSpeed_kmph") and + type(find_path(self.attrs, "trip_last.averageSpeed_kmph")) in (float, int) + ) @property def trip_last_average_electric_engine_consumption(self): @@ -2121,13 +2127,12 @@ def trip_last_average_electric_engine_consumption(self): :return: """ - value = self.trip_last_entry.get("averageElectricEngineConsumption") - return float(value / 10) + return float(find_path(self.attrs, "trip_last.averageElectricConsumption")) @property def trip_last_average_electric_engine_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_average_electric_engine_consumption_supported(self) -> bool: @@ -2136,8 +2141,10 @@ def is_trip_last_average_electric_engine_consumption_supported(self) -> bool: :return: """ - response = self.trip_last_entry - return response and type(response.get("averageElectricEngineConsumption", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.averageElectricConsumption") and + type(find_path(self.attrs, "trip_last.averageElectricConsumption")) in (float, int) + ) @property def trip_last_average_fuel_consumption(self): @@ -2146,12 +2153,12 @@ def trip_last_average_fuel_consumption(self): :return: """ - return int(self.trip_last_entry.get("averageFuelConsumption")) / 10 + return float(find_path(self.attrs, "trip_last.averageFuelConsumption")) @property def trip_last_average_fuel_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_average_fuel_consumption_supported(self) -> bool: @@ -2160,11 +2167,9 @@ def is_trip_last_average_fuel_consumption_supported(self) -> bool: :return: """ - response = self.trip_last_entry return ( - self.has_combustion_engine() - and response - and type(response.get("averageFuelConsumption", None)) in (float, int) + is_valid_path(self.attrs, "trip_last.averageFuelConsumption") and + type(find_path(self.attrs, "trip_last.averageFuelConsumption")) in (float, int) ) @property @@ -2174,12 +2179,14 @@ def trip_last_average_auxillary_consumption(self): :return: """ + # no example verified yet return self.trip_last_entry.get("averageAuxiliaryConsumption") @property def trip_last_average_auxillary_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") + @property def is_trip_last_average_auxillary_consumption_supported(self) -> bool: @@ -2188,8 +2195,11 @@ def is_trip_last_average_auxillary_consumption_supported(self) -> bool: :return: """ - response = self.trip_last_entry - return response and type(response.get("averageAuxiliaryConsumption", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.averageAuxiliaryConsumption") and + type(find_path(self.attrs, "trip_last.averageAuxiliaryConsumption")) in (float, int) + ) + @property def trip_last_average_aux_consumer_consumption(self): @@ -2198,15 +2208,13 @@ def trip_last_average_aux_consumer_consumption(self): :return: """ - value = self.trip_last_entry.get("averageAuxConsumerConsumption") - if value == 65535: - return None - return float(value / 10) + # no example verified yet + return self.trip_last_entry.get("averageAuxConsumerConsumption") @property def trip_last_average_aux_consumer_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_average_aux_consumer_consumption_supported(self) -> bool: @@ -2215,10 +2223,10 @@ def is_trip_last_average_aux_consumer_consumption_supported(self) -> bool: :return: """ - response = self.trip_last_entry - if response.get("averageAuxConsumerConsumption", 65535) == 65535: - return False - return response and type(response.get("averageAuxConsumerConsumption", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.averageAuxConsumerConsumption") and + type(find_path(self.attrs, "trip_last.averageAuxConsumerConsumption")) in (float, int) + ) @property def trip_last_duration(self): @@ -2227,12 +2235,13 @@ def trip_last_duration(self): :return: """ - return self.trip_last_entry.get("traveltime") + return find_path(self.attrs, "trip_last.travelTime") + @property def trip_last_duration_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_duration_supported(self) -> bool: @@ -2241,8 +2250,11 @@ def is_trip_last_duration_supported(self) -> bool: :return: """ - response = self.trip_last_entry - return response and type(response.get("traveltime", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.travelTime") and + type(find_path(self.attrs, "trip_last.travelTime")) in (float, int) + ) + @property def trip_last_length(self): @@ -2251,12 +2263,13 @@ def trip_last_length(self): :return: """ - return self.trip_last_entry.get("mileage") + return find_path(self.attrs, "trip_last.mileage_km") @property def trip_last_length_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") + @property def is_trip_last_length_supported(self) -> bool: @@ -2265,8 +2278,11 @@ def is_trip_last_length_supported(self) -> bool: :return: """ - response = self.trip_last_entry - return response and type(response.get("mileage", None)) in (float, int) + return ( + is_valid_path(self.attrs, "trip_last.mileage_km") and + type(find_path(self.attrs, "trip_last.mileage_km")) in (float, int) + ) + @property def trip_last_recuperation(self): @@ -2281,7 +2297,7 @@ def trip_last_recuperation(self): @property def trip_last_recuperation_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_recuperation_supported(self) -> bool: @@ -2307,7 +2323,7 @@ def trip_last_average_recuperation(self): @property def trip_last_average_recuperation_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_average_recuperation_supported(self) -> bool: @@ -2332,7 +2348,7 @@ def trip_last_total_electric_consumption(self): @property def trip_last_total_electric_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" - return self.trip_last_entry.get("timestamp") + return find_path(self.attrs, "trip_last.tripEndTimestamp") @property def is_trip_last_total_electric_consumption_supported(self) -> bool: From 0a8290e1afc64cf0fcbb52afe68514e562300ad7 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Wed, 29 Nov 2023 11:41:11 +0100 Subject: [PATCH 12/41] some window heater fixes --- volkswagencarnet/vw_dashboard.py | 14 -------------- volkswagencarnet/vw_vehicle.py | 6 +++--- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/volkswagencarnet/vw_dashboard.py b/volkswagencarnet/vw_dashboard.py index 1d205649..f7fb88f4 100644 --- a/volkswagencarnet/vw_dashboard.py +++ b/volkswagencarnet/vw_dashboard.py @@ -772,20 +772,6 @@ def create_instruments(): TrunkLock(), RequestUpdate(), WindowHeater(), - BinarySensor( - attr="window_heater_front", - name="Window Heater Front", - device_class=VWDeviceClass.WINDOW, - icon="mdi:car-defrost-front", - reverse_state=True - ), - BinarySensor( - attr="window_heater_back", - name="Window Heater Back", - device_class=VWDeviceClass.WINDOW, - icon="mdi:car-defrost-rear", - reverse_state=True - ), BatteryClimatisation(), ElectricClimatisation(), AuxiliaryClimatisation(), diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 8e27e8bb..0bcbffc4 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1359,12 +1359,12 @@ def window_heater_back(self) -> bool: return False @property - def window_heater_rear_last_updated(self) -> datetime: + def window_heater_back_last_updated(self) -> datetime: """Return front window heater last updated.""" return find_path(self.attrs, "climatisation.windowHeatingStatus.value.carCapturedTimestamp") @property - def is_window_heater_rear_supported(self) -> bool: + def is_window_heater_back_supported(self) -> bool: """Return true if vehicle has heater.""" return is_valid_path(self.attrs, "climatisation.windowHeatingStatus.value.windowHeatingStatus") @@ -1379,7 +1379,7 @@ def window_heater_last_updated(self) -> datetime: return self.window_heater_front_last_updated @property - def is_window_supported(self) -> bool: + def is_window_heater_supported(self) -> bool: """Return true if vehicle has heater.""" return self.is_window_heater_front_supported From 2a5fbbb26a9beadafe05c4a9d1afb4e7a4db9cd1 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:47:11 +0100 Subject: [PATCH 13/41] Fix fuel sensor for combustion car (#217) --- volkswagencarnet/vw_vehicle.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 0bcbffc4..8c1d5086 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1192,12 +1192,24 @@ def fuel_level(self) -> int: :return: """ - return int(find_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct")) + fuel_level_pct = "" + if is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct"): + fuel_level_pct = find_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct") + + if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct"): + fuel_level_pct = find_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") + return int(fuel_level_pct) @property def fuel_level_last_updated(self) -> datetime: """Return fuel level last updated.""" - return find_path(self.attrs, "measurements.fuelLevelStatus.value.carCapturedTimestamp") + fuel_level_lastupdated = "" + if is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.carCapturedTimestamp"): + fuel_level_lastupdated = find_path(self.attrs, "fuelStatus.rangeStatus.value.carCapturedTimestamp") + + if is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.carCapturedTimestamp"): + fuel_level_lastupdated = find_path(self.attrs, "measurements.fuelLevelStatus.value.carCapturedTimestamp") + return fuel_level_lastupdated @property def is_fuel_level_supported(self) -> bool: @@ -1206,7 +1218,8 @@ def is_fuel_level_supported(self) -> bool: :return: """ - return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") + return (is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") + or is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct")) # Climatisation settings @property From 3f08dfaf9d555810c7e3e9bf1e5341ce73d4f216 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Sun, 3 Dec 2023 22:03:16 +0100 Subject: [PATCH 14/41] remove two unused services from polling --- volkswagencarnet/vw_vehicle.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 8c1d5086..e383d774 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -159,14 +159,12 @@ async def update(self): await self.discover() if not self.deactivated: await asyncio.gather( - # TODO: we don't check against capabilities currently, but this also doesn't seem to be neccesary + # TODO: we don't check against capabilities currently, but this also doesn't seem to be necessary # to be checked if we should still do it for UI purposes self.get_selectivestatus( [ "access", "fuelStatus", - "honkAndFlash", - "userCapabilities", "vehicleLights", "vehicleHealthInspection", "measurements", From 6d76601485f3311d94bd8fcbaf912013a7f928f8 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Mon, 4 Dec 2023 19:58:19 +0100 Subject: [PATCH 15/41] some clean-up --- volkswagencarnet/vw_connection.py | 119 +++--------------------------- volkswagencarnet/vw_const.py | 17 +---- volkswagencarnet/vw_vehicle.py | 4 +- 3 files changed, 14 insertions(+), 126 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 33558ab0..76b0b17e 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -187,11 +187,6 @@ def base64URLEncode(s): allow_redirects=False, params={ "redirect_uri": APP_URI, - # "prompt": "login", - # "nonce": getNonce(), - # "state": getNonce(), - # "code_challenge_method": "s256", - # "code_challenge": challenge.decode(), "response_type": CLIENT[client].get("TOKEN_TYPES"), "client_id": CLIENT[client].get("CLIENT_ID"), "scope": CLIENT[client].get("SCOPE"), @@ -340,7 +335,6 @@ def base64URLEncode(s): ) if req.status != 200: raise Exception(f"Token exchange failed. Received message: {await req.content.read()}") - # Save tokens as "identity", these are tokens representing the user self._session_tokens[client] = await req.json() if "error" in self._session_tokens[client]: error_msg = self._session_tokens[client].get("error", "") @@ -364,56 +358,6 @@ def base64URLEncode(s): self._session_headers["Authorization"] = "Bearer " + self._session_tokens[client]["access_token"] return True - async def _getAPITokens(self): - try: - # Get VW Group API tokens - # https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token - tokenBody2 = { - "grant_type": "id_token", - "token": self._session_tokens["identity"]["id_token"], - "scope": "sc2:fal", - } - _LOGGER.debug("Trying to fetch api tokens.") - req = await self._session.post( - url="https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token", - headers={ - "User-Agent": USER_AGENT, - "X-App-Version": XAPPVERSION, - "X-App-Name": XAPPNAME, - "X-Client-Id": XCLIENT_ID, - }, - data=tokenBody2, - allow_redirects=False, - ) - if req.status > 400: - _LOGGER.debug("API token request failed.") - raise Exception(f"API token request returned with status code {req.status}") - else: - # Save tokens as "vwg", use these for get/posts to VW Group API - self._session_tokens["vwg"] = await req.json() - if "error" in self._session_tokens["vwg"]: - error = self._session_tokens["vwg"].get("error", "") - if "error_description" in self._session_tokens["vwg"]: - error_description = self._session_tokens["vwg"].get("error_description", "") - raise Exception(f"{error} - {error_description}") - else: - raise Exception(error) - if self._session_fulldebug: - for token in self._session_tokens.get("vwg", {}): - _LOGGER.debug(f"Got token {token}") - if not await self.verify_tokens(self._session_tokens["vwg"].get("access_token", ""), "vwg"): - _LOGGER.warning("VW-Group API token could not be verified!") - else: - _LOGGER.debug("VW-Group API token verified OK.") - - # Update headers for requests, defaults to using VWG token - self._session_headers["Authorization"] = "Bearer " + self._session_tokens["vwg"]["access_token"] - except Exception as error: - _LOGGER.error(f"Failed to fetch VW-Group API tokens, {error}") - self._session_logged_in = False - return False - return True - async def terminate(self): """Log out from connect services.""" _LOGGER.info("Initiating logout") @@ -421,35 +365,21 @@ async def terminate(self): async def logout(self): """Logout, revoke tokens.""" + # TODO: not tested yet self._session_headers.pop("Authorization", None) if self._session_logged_in: - if self._session_headers.get("vwg", {}).get("access_token"): - _LOGGER.info("Revoking API Access Token...") - self._session_headers["token_type_hint"] = "access_token" - params = {"token": self._session_tokens["vwg"]["access_token"]} - await self.post( - "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/revoke", data=params - ) - if self._session_headers.get("vwg", {}).get("refresh_token"): - _LOGGER.info("Revoking API Refresh Token...") - self._session_headers["token_type_hint"] = "refresh_token" - params = {"token": self._session_tokens["vwg"]["refresh_token"]} - await self.post( - "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/revoke", data=params - ) - self._session_headers.pop("token_type_hint", None) if self._session_headers.get("identity", {}).get("identity_token"): _LOGGER.info("Revoking Identity Access Token...") # params = { # "token": self._session_tokens['identity']['access_token'], # "brand": BRAND # } - # revoke_at = await self.post('https://tokenrefreshservice.apps.emea.vwapps.io/revokeToken', data = params) + # revoke_at = await self.post('https://emea.bff.cariad.digital/login/v1/idk/revoke', data = params) if self._session_headers.get("identity", {}).get("refresh_token"): _LOGGER.info("Revoking Identity Refresh Token...") - params = {"token": self._session_tokens["identity"]["refresh_token"], "brand": BRAND} - await self.post("https://tokenrefreshservice.apps.emea.vwapps.io/revokeToken", data=params) + params = {"token": self._session_tokens["identity"]["refresh_token"]} + await self.post("https://emea.bff.cariad.digital/login/v1/idk/revoke", data=params) # HTTP methods to API async def _request(self, method, url, **kwargs): @@ -474,21 +404,21 @@ async def _request(self, method, url, **kwargs): try: if response.status == 204: - res = {"status_code": response.status} + res = {"status_code": response.status, "body": response.text} elif response.status >= 200 or response.status <= 300: res = await response.json(loads=json_loads) else: res = {} - _LOGGER.debug(f"Not success status code [{response.status}] response: {response}") + _LOGGER.debug(f"Not success status code [{response.status}] response: {response.text}") if "X-RateLimit-Remaining" in response.headers: res["rate_limit_remaining"] = response.headers.get("X-RateLimit-Remaining", "") except Exception: res = {} - _LOGGER.debug(f"Something went wrong [{response.status}] response: {response}") + _LOGGER.debug(f"Something went wrong [{response.status}] response: {response.text}") return res if self._session_fulldebug: - _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res}') + _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res.text}') else: _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]') return res @@ -1246,10 +1176,6 @@ async def verify_tokens(self, token, type, client="Legacy"): "https://api.vas.eu.dp15.vwg-connect.com", "https://api.vas.eu.wcardp.io", ] - elif type == "vwg": - req = await self._session.get(url="https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/public/jwk/v1") - keys = await req.json() - audience = "mal.prd.ece.vwg-connect.com" else: _LOGGER.debug("Not implemented") return False @@ -1261,8 +1187,6 @@ async def verify_tokens(self, token, type, client="Legacy"): pubkeys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk(to_json(jwk)) token_kid = jwt.get_unverified_header(token)["kid"] - if type == "vwg": - token_kid = "VWGMBB01DELIV1." + token_kid pubkey = pubkeys[token_kid] jwt.decode(token, key=pubkey, algorithms=JWT_ALGORITHMS, audience=audience) @@ -1286,11 +1210,11 @@ async def refresh_tokens(self): body = { "grant_type": "refresh_token", - # "brand": BRAND, "refresh_token": self._session_tokens["identity"]["refresh_token"], + "client_id": XCLIENT_ID } response = await self._session.post( - url="https://tokenrefreshservice.apps.emea.vwapps.io/refreshTokens", headers=tHeaders, data=body + url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body ) if response.status == 200: tokens = await response.json() @@ -1303,34 +1227,11 @@ async def refresh_tokens(self): _LOGGER.warning(f"Something went wrong when refreshing {BRAND} account tokens.") return False - body = {"grant_type": "id_token", "scope": "sc2:fal", "token": self._session_tokens["identity"]["id_token"]} - - response = await self._session.post( - url="https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token", - headers=tHeaders, - data=body, - allow_redirects=True, - ) - if response.status == 200: - tokens = await response.json() - if not await self.verify_tokens(tokens["access_token"], "vwg"): - _LOGGER.warning("Token could not be verified!") - for token in tokens: - self._session_tokens["vwg"][token] = tokens[token] - else: - resp = await response.text() - _LOGGER.warning("Something went wrong when refreshing API tokens. %s" % resp) - return False return True except Exception as error: _LOGGER.warning(f"Could not refresh tokens: {error}") return False - async def set_token(self, type): - """Switch between tokens.""" - self._session_headers["Authorization"] = "Bearer " + self._session_tokens[type]["access_token"] - return - # Class helpers # @property def vehicles(self): diff --git a/volkswagencarnet/vw_const.py b/volkswagencarnet/vw_const.py index 01d42f49..99d9b6cf 100644 --- a/volkswagencarnet/vw_const.py +++ b/volkswagencarnet/vw_const.py @@ -10,22 +10,9 @@ CLIENT = { "Legacy": { "CLIENT_ID": "a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com", - # client id for VWG API, legacy Skoda Connect/MySkoda "SCOPE": "openid profile badge cars dealers vin", - # 'SCOPE': 'openid mbb profile cars address email birthdate badge phone driversLicense dealers profession vin', - "TOKEN_TYPES": "code", # id_token token", - }, - "New": { - "CLIENT_ID": "f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com", - # Provides access to new API? tokentype=IDK_TECHNICAL.. - "SCOPE": "openid mbb profile", - "TOKEN_TYPES": "code id_token", - }, - "Unknown": { - "CLIENT_ID": "72f9d29d-aa2b-40c1-bebe-4c7683681d4c@apps_vw-dilab_com", # gives tokentype=IDK_SMARTLINK ? - "SCOPE": "openid dealers profile email cars address", - "TOKEN_TYPES": "code id_token", - }, + "TOKEN_TYPES": "code" + } } diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index e383d774..acf589ad 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -386,7 +386,7 @@ async def set_charge_min_level(self, level: int): async def set_charger(self, action) -> bool: """Charging actions.""" - if not self._services.get("rbatterycharge_v1", False): + if not self._services.get("charging", False): _LOGGER.info("Remote start/stop of charger is not supported.") raise Exception("Remote start/stop of charger is not supported.") if self._in_progress("batterycharge"): @@ -480,7 +480,7 @@ async def set_climatisation(self, mode="off", spin=False): async def set_climater(self, data, spin=False): """Climater actions.""" - if not self._services.get("rclima_v1", False): + if not self._services.get("climatisation", False): _LOGGER.info("Remote control of climatisation functions is not supported.") raise Exception("Remote control of climatisation functions is not supported.") if self._in_progress("climatisation"): From b489b17cdff1124d2589c15933fa51486f6d2fd9 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 5 Dec 2023 08:00:10 +0100 Subject: [PATCH 16/41] more clean-up and fixed refresh_tokens --- volkswagencarnet/vw_connection.py | 8 +------- volkswagencarnet/vw_const.py | 10 ---------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 76b0b17e..1d3b1dbd 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -32,9 +32,6 @@ BASE_API, BASE_AUTH, CLIENT, - XCLIENT_ID, - XAPPVERSION, - XAPPNAME, USER_AGENT, APP_URI, ) @@ -1203,15 +1200,12 @@ async def refresh_tokens(self): "Connection": "keep-alive", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT, - "X-App-Version": XAPPVERSION, - "X-App-Name": XAPPNAME, - "X-Client-Id": XCLIENT_ID, } body = { "grant_type": "refresh_token", "refresh_token": self._session_tokens["identity"]["refresh_token"], - "client_id": XCLIENT_ID + "client_id": CLIENT["Legacy"]["CLIENT_ID"] } response = await self._session.post( url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body diff --git a/volkswagencarnet/vw_const.py b/volkswagencarnet/vw_const.py index 99d9b6cf..861bc962 100644 --- a/volkswagencarnet/vw_const.py +++ b/volkswagencarnet/vw_const.py @@ -15,10 +15,6 @@ } } - -XCLIENT_ID = "c8fcb3bf-22d3-44b0-b6ce-30eae0a4986f" -XAPPVERSION = "5.3.2" -XAPPNAME = "We Connect" USER_AGENT = "Volkswagen/2.20.0 iOS/17.1.1" APP_URI = "weconnect://authenticated" @@ -28,9 +24,6 @@ "Content-Type": "application/json", "Accept-charset": "UTF-8", "Accept": "application/json", - "X-Client-Id": XCLIENT_ID, - "X-App-Version": XAPPVERSION, - "X-App-Name": XAPPNAME, "User-Agent": USER_AGENT, "tokentype": "IDK_TECHNICAL", } @@ -41,9 +34,6 @@ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded", - "x-requested-with": XAPPNAME, - "User-Agent": USER_AGENT, - "X-App-Name": XAPPNAME, } TEMP_CELSIUS: str = "°C" From 7c1fa7d9377671424638abfda0b8dd6c0549716a Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Tue, 5 Dec 2023 11:39:10 +0100 Subject: [PATCH 17/41] Fix Login race condition (#220) --- volkswagencarnet/vw_connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 1d3b1dbd..19f65c3f 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -110,6 +110,7 @@ async def doLogin(self, tries: int = 1): # Add Vehicle class object for all VIN-numbers from account if loaded_vehicles.get("data") is not None: _LOGGER.debug("Found vehicle(s) associated with account.") + self._vehicles = [] for vehicle in loaded_vehicles.get("data"): self._vehicles.append(Vehicle(self, vehicle.get("vin"))) else: From 68c9fd7a780a77572171e69710642c7c9e5c9f7d Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:47:35 +0100 Subject: [PATCH 18/41] Fix the bug introduced with the clean-up commit 3c753a3 (#221) --- volkswagencarnet/vw_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 19f65c3f..5a27763e 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -416,7 +416,7 @@ async def _request(self, method, url, **kwargs): return res if self._session_fulldebug: - _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res.text}') + _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res}') else: _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]') return res From 7786f4f33ff30d4ffac09343215b824bc202a9c7 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Tue, 5 Dec 2023 16:36:30 +0100 Subject: [PATCH 19/41] added some response examples --- rate_limit_analysis.py | 63 ++ refresh_test.py | 34 + tests/fixtures/resources/responses/README.md | 26 + .../arteon_2023_diesel/capabilities.json | 426 ++++++++++ .../arteon_2023_diesel/last_trip.json | 14 + .../arteon_2023_diesel/parkingposition.json | 1 + .../selectivestatus_by_app.json | 284 +++++++ .../responses/eup_electric/capabilities.json | 607 ++++++++++++++ .../responses/eup_electric/last_trip.json | 16 + .../eup_electric/parkingposition.json | 1 + .../eup_electric/selectivestatus_by_app.json | 395 +++++++++ .../golf_gte_hybrid/capabilities.json | 791 ++++++++++++++++++ .../responses/golf_gte_hybrid/last_trip.json | 15 + .../selectivestatus_by_app.json | 557 ++++++++++++ 14 files changed, 3230 insertions(+) create mode 100644 rate_limit_analysis.py create mode 100644 refresh_test.py create mode 100644 tests/fixtures/resources/responses/README.md create mode 100644 tests/fixtures/resources/responses/arteon_2023_diesel/capabilities.json create mode 100644 tests/fixtures/resources/responses/arteon_2023_diesel/last_trip.json create mode 100644 tests/fixtures/resources/responses/arteon_2023_diesel/parkingposition.json create mode 100644 tests/fixtures/resources/responses/arteon_2023_diesel/selectivestatus_by_app.json create mode 100644 tests/fixtures/resources/responses/eup_electric/capabilities.json create mode 100644 tests/fixtures/resources/responses/eup_electric/last_trip.json create mode 100644 tests/fixtures/resources/responses/eup_electric/parkingposition.json create mode 100644 tests/fixtures/resources/responses/eup_electric/selectivestatus_by_app.json create mode 100644 tests/fixtures/resources/responses/golf_gte_hybrid/capabilities.json create mode 100644 tests/fixtures/resources/responses/golf_gte_hybrid/last_trip.json create mode 100644 tests/fixtures/resources/responses/golf_gte_hybrid/selectivestatus_by_app.json diff --git a/rate_limit_analysis.py b/rate_limit_analysis.py new file mode 100644 index 00000000..6971dfa0 --- /dev/null +++ b/rate_limit_analysis.py @@ -0,0 +1,63 @@ +from volkswagencarnet.vw_connection import Connection +import volkswagencarnet.vw_const as const +from tests.credentials import username, password + +from aiohttp import ClientSession +import pprint +import asyncio +import logging + +logging.basicConfig(level=logging.DEBUG) + +VW_USERNAME=username +VW_PASSWORD=password + +SERVICES = ["access", + "fuelStatus", + "vehicleLights", + "vehicleHealthInspection", + "measurements", + "charging", + "climatisation", + "automation"] + +async def main(): + """Main method.""" + async with ClientSession(headers={'Connection': 'keep-alive'}) as session: + connection = Connection(session, VW_USERNAME, VW_PASSWORD) + request_count = 0 + if await connection.doLogin(): + logging.info(f"Logged in to account {VW_USERNAME}") + logging.info("Tokens:") + logging.info(pprint.pformat(connection._session_tokens)) + + vehicle = connection.vehicles[0].vin + error_count = 0 + while True: + await connection.validate_tokens + #response = await connection.get( + # f"{const.BASE_API}/vehicle/v1/vehicles/{vehicle}/selectivestatus?jobs={','.join(SERVICES)}", "" + #)ng + response = await connection.get( + f"{const.BASE_API}/vehicle/v1/trips/{vehicle}/shortterm/last", "" + ) + + + request_count += 1 + logging.info(f"Request count is {request_count} with {len(SERVICES)} services, response: {pprint.pformat(response)[1:100]}") + if "status_code" in response: + if response["status_code"] != 403: + logging.error(f"Something went wrong, received status code {response.get('status_code')}, bailing out") + exit(-1) + error_count += 1 + if error_count > 3: + logging.error("More than 3 errors in a row, bailing out") + else: + error_count = 0 + + await asyncio.sleep(1) + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + # loop.run(main()) + loop.run_until_complete(main()) \ No newline at end of file diff --git a/refresh_test.py b/refresh_test.py new file mode 100644 index 00000000..756dbe1b --- /dev/null +++ b/refresh_test.py @@ -0,0 +1,34 @@ +from volkswagencarnet.vw_connection import Connection +import volkswagencarnet.vw_const as const +from tests.credentials import username, password + +from aiohttp import ClientSession +import pprint +import asyncio +import logging + +logging.basicConfig(level=logging.DEBUG) + +VW_USERNAME=username +VW_PASSWORD=password + + +async def main(): + """Main method.""" + async with ClientSession(headers={'Connection': 'keep-alive'}) as session: + connection = Connection(session, VW_USERNAME, VW_PASSWORD) + request_count = 0 + connection._session_logged_in = True + connection._session_tokens["Legacy"] = dict() + connection._session_tokens["Legacy"]["access_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwNjNkODdkMS1lNTc4LTRiNDMtOTEzNC1kY2YyNzVlNWQxOTAiLCJhdWQiOiJhMjRmYmE2My0zNGIzLTRkNDMtYjE4MS05NDIxMTFlNmJkYThAYXBwc192dy1kaWxhYl9jb20iLCJzY3AiOiJvcGVuaWQgcHJvZmlsZSBiYWRnZSBjYXJzIGRlYWxlcnMgdmluIiwiYWF0IjoiaWRlbnRpdHlraXQiLCJpc3MiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8iLCJqdHQiOiJhY2Nlc3NfdG9rZW4iLCJleHAiOjE3MDE3NTkwNTEsImlhdCI6MTcwMTc1NTQ1MSwibGVlIjpbIlZPTEtTV0FHRU4iXSwianRpIjoiZDJiMGEwN2QtMjJlMS00ZjY3LTk3MWEtNjJhNjIzMDU1YjE5In0.VGwJoHlxy3FUqLIWGh82Lo0yxsJXf9gRBMDhqf8BqE4-de_myCzBdgwXVUgHMaDWIlvVArEXlGQhnvse7JArqgiYt0bjfaXhlZh8x2F8R9HcoLmKgqHoUJRyuUSq5LZyvzKgrw9k7FDmqgCRju8vZhvb3wRcmVZ89tirI9xvoyF65nCkMg_10nhRcEfu_JBf-8xNEZdMtgFg4zVCXYnckd9KrhCLmRvBc1bsTuaLnxrU56DD_yORMhmHBI39SXh0ME1bfL7Td2aadvIJJovA0KoEVdrKUOyCrmxb87Kam19OR36Um1zVe0GVGj4LPxf-NtQwSevBOQ4Px0Ti0iFvKORct4gj2bhKW8wOX7r8llLmALcCNToNYhUJSFAkqgMJWWk9fRu7Acn-RAcIF20tIe9KdO5yq3gkanqB1n2RoSNmoRl-C0ltjtdsc0peOz3U3wjxwFA3nSz7RndfY21rOTOfCq9yiFT0XO65dQ_w3_yH3UoMAArxYwR_0mJr4pC6EYmr92EHkn8WR3n9P6NAw0fFMzPNLoCszOCLNZvQND3abLmO4f0kw4mjJK9vZTFI2_JjAEKNE7hqXW_c9YzngTvltNLsHx67xZHql3ywMZvIYCrO_UfKA65NLdfmarYo32E0-EjonozwPaJPWGM-tKvwqlXJjHPpLRdHOU3SGNs' + connection._session_tokens["Legacy"]["refresh_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwNjNkODdkMS1lNTc4LTRiNDMtOTEzNC1kY2YyNzVlNWQxOTAiLCJhdWQiOiJhMjRmYmE2My0zNGIzLTRkNDMtYjE4MS05NDIxMTFlNmJkYThAYXBwc192dy1kaWxhYl9jb20iLCJhY3IiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8vYXNzdXJhbmNlL2xvYS0yIiwic2NwIjoib3BlbmlkIHByb2ZpbGUgYmFkZ2UgY2FycyBkZWFsZXJzIHZpbiIsImFhdCI6ImlkZW50aXR5a2l0IiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS52d2dyb3VwLmlvIiwianR0IjoicmVmcmVzaF90b2tlbiIsImV4cCI6MTcxNzMwNzQ1MSwiaWF0IjoxNzAxNzU1NDUxLCJqdGkiOiJiMzEyM2IyMy04MDE0LTQ2MmMtYjEzNC01Mzk0ODFlNjQ1N2QifQ.K3Pb4wzqRAz9UIDtPFm8HA2iVNzBWf5ZL2_9U5tKwF1eawd8j3j8I1OBqhBVXuF6wAHlCckpDb99o6JVEC2KsdfJFMOkVj_84s5UKyHCXL-PZc0W-TSslaR9V0dH0TkAgffC3eP2wFlQOwjGKwxj1ljT_2i66oo-hj8TcjSAjeTAIcAu-ySWDL4MFb4vo4omdtAAXzOq677p9JTkP2CSpkkjELrmyjhXb-2zEQcLEZxLEy5eD01YwzhDVLPV_Kti2nYPjSASfgd9pTE0rkIpQdCSQJKpWF0XqYIlRM3RbNekeVTDxD4GK77X3LlEb_VXfuNHm9ETS02LiV2C9Qt79ol4MKVv6Ij9fF0fuAsVXbz7Ft3AH413bjy-rhuP6y8fY-wTf2MCplljmk4U9D2m0mZeAfKnRqv6Z_mUrpY-0bB9gN9pq9T5Mm7f29i1qZYDS5XDwar2muH50LwL3jQj6rtCVImTXpxN8Jlh7cuBIx3IRHjqskjBf0P_SwFhUNFkSzQPj56ib3Lle6HmzaosmWaWU9fyO2OR4rBR2PLfdQrDudTFeLKYwUUQEXbz5OwhsZt6G0woKYnpYneYDyYTewHvsltsnbGsn00bzEgob3552QK-IKIEYDS8ZwVCDkhhKfB66kyruvEBeP2dAEopDgNYR2jcxWtYlM0Tz6OuAVs' + connection._session_tokens["Legacy"]["id_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoibjNTRTY5b1Jac1VLaFZITzE1NHlzQSIsInN1YiI6IjA2M2Q4N2QxLWU1NzgtNGI0My05MTM0LWRjZjI3NWU1ZDE5MCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8iLCJqdHQiOiJpZF90b2tlbiIsImxlZSI6WyJWT0xLU1dBR0VOIl0sImF1ZCI6ImEyNGZiYTYzLTM0YjMtNGQ0My1iMTgxLTk0MjExMWU2YmRhOEBhcHBzX3Z3LWRpbGFiX2NvbSIsImFjciI6Imh0dHBzOi8vaWRlbnRpdHkudndncm91cC5pby9hc3N1cmFuY2UvbG9hLTIiLCJ1cGRhdGVkX2F0IjoxNjc2NDc5MzMxMDMzLCJhYXQiOiJpZGVudGl0eWtpdCIsImV4cCI6MTcwMTc1OTA1MSwiaWF0IjoxNzAxNzU1NDUxLCJqdGkiOiI2ZGQxYWE1Yy0wZWU4LTRlYTktOTY5Yi1jZjA5N2ZjOGViYzUiLCJlbWFpbCI6Im9saXZlckByYWhuZXIubWUifQ.d_3ySV9qle_rN-9wKhzEAa-RuHn0exsdzv7Ilrg2d8dcPfa7alzYAauIH-ZAM86x7O_F8G3fuTMnFK9tjCmhhhCQbvMfSuCQsId0rlyJIufNEwQZkzHXqYx2e8_zSRCDIZXD_uE3KHfe0vSKXEgfB2ijchgMdclkjF0W6TaDVBiaqKg9SX6IvSGcWS__aNcKBsi177GHq6WPDtuvaOdjFjS-AzwGq_ktcFvNpkkIy2Vfu3ZsYtetcupRQ03njKIX_c0O3_OBAducvue0YhuDeU-0gXZwtymyiDjcjYe8eLzNMMYGe9gU-IR10BZjWI7FM7QeRShg8vuNGAzxR4nyonhBct6KSKS66a0aIIQ2hqJYxhBe9yKpkCz5nlUkrY1r0TtVW1OfLYXwVaFaGdD_9rc4UVMMRVsA1jRK2REg1EyzT-mto0MfsTZG9Os49_wPYY4hBqwAXuHRFSvC3-Zt56IL--N2u9VCJU32l9t9rSL0iWnV8sVGZ2r800U4CWa42ycvT-CbPUGvQCMxtY2q3DltZe1D6yeKkxs5N0S24qc2TYuvEztwWCiE_bkd9ZPJeOAdTX3OORsgIeOjtdweyKBJ-zswjyj4c3JIAimwtu1b9sQnMzzqZdevtWkd3Ogh0VLX6uWaVzInHvmH96DePlcA1zm8PWnvgBdYAJ_78pQ' + connection._session_tokens["identity"] = connection._session_tokens["Legacy"].copy() + + await connection.validate_tokens + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + # loop.run(main()) + loop.run_until_complete(main()) \ No newline at end of file diff --git a/tests/fixtures/resources/responses/README.md b/tests/fixtures/resources/responses/README.md new file mode 100644 index 00000000..e49993d3 --- /dev/null +++ b/tests/fixtures/resources/responses/README.md @@ -0,0 +1,26 @@ +# Example Responses + +The sub-directories contain some examples for responses of VW's API for certain car types: + +* [Arteon Diesel](arteon_2023_diesel) +* [eUP! Electric](eup_electric) +* [Golf GTE Hybrid](golf_gte_hybrid) + +## Files + +`capabilities.json` + +Response to the GET request to https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/capabilities + +`last_trip.json` + +Response to the GET request to https://emea.bff.cariad.digital/vehicle/v1/trips/{vin}/shortterm/last + +`parkingposition.json` + +Response to the GET request to https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/parkingposition + +`selectivestatus_by_app.json` + +Response to the GET request to https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/selectivestatus?jobs=XXX. +The exact URL is the one the Volkswagen app fires for the respective car type. \ No newline at end of file diff --git a/tests/fixtures/resources/responses/arteon_2023_diesel/capabilities.json b/tests/fixtures/resources/responses/arteon_2023_diesel/capabilities.json new file mode 100644 index 00000000..22449fcb --- /dev/null +++ b/tests/fixtures/resources/responses/arteon_2023_diesel/capabilities.json @@ -0,0 +1,426 @@ +{ + "vin": "WVWZZZ0XXXX000000", + "capabilities": { + "destinations": { + "id": "destinations", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getDestinations": { + "id": "getDestinations", + "scopes": [ + "navigation" + ] + }, + "postDestinations": { + "id": "postDestinations", + "scopes": [ + "navigation" + ] + }, + "putDestinations": { + "id": "putDestinations", + "scopes": [ + "navigation" + ] + }, + "deleteDestinationsByID": { + "id": "deleteDestinationsByID", + "scopes": [ + "navigation" + ] + } + }, + "parameters": [ + { + "key": "chargingStationsForEVTourImport", + "value": "" + } + ] + }, + "roadsideAssistant": { + "id": "roadsideAssistant", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": {}, + "parameters": [] + }, + "fuelStatus": { + "id": "fuelStatus", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getFuelStatus": { + "id": "getFuelStatus", + "scopes": [ + "fuelLevels" + ] + } + }, + "parameters": [] + }, + "honkAndFlash": { + "id": "honkAndFlash", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postHonkandflash": { + "id": "postHonkandflash", + "scopes": [ + "honk" + ] + }, + "getHonkandflashRequestsByID": { + "id": "getHonkandflashRequestsByID", + "scopes": [ + "honk" + ] + } + }, + "parameters": [] + }, + "measurements": { + "id": "measurements", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMeasurements": { + "id": "getMeasurements", + "scopes": [ + "range", + "mileage" + ] + } + }, + "parameters": [] + }, + "parkingPosition": { + "id": "parkingPosition", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getParkingposition": { + "id": "getParkingposition", + "scopes": [ + "parking_position" + ] + } + }, + "parameters": [] + }, + "state": { + "id": "state", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getAccessStatus": { + "id": "getAccessStatus", + "scopes": [ + "doors_windows" + ] + } + }, + "parameters": [] + }, + "vehicleHealth": { + "id": "vehicleHealth", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": {}, + "parameters": [] + }, + "vehicleHealthInspection": { + "id": "vehicleHealthInspection", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMaintenanceStatus": { + "id": "getMaintenanceStatus", + "scopes": [ + "serviceInterval" + ] + } + }, + "parameters": [] + }, + "vehicleHealthWarnings": { + "id": "vehicleHealthWarnings", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getWarninglightsLast": { + "id": "getWarninglightsLast", + "scopes": [ + "warning_lights" + ] + } + }, + "parameters": [] + }, + "vehicleWakeUpTrigger": { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeupUpdate": { + "id": "postVehiclewakeupUpdate", + "scopes": [] + } + }, + "parameters": [] + }, + "vehicleWakeUp": { + "id": "vehicleWakeUp", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeup": { + "id": "postVehiclewakeup", + "scopes": [] + }, + "getVehiclewakeupRequestsByID": { + "id": "getVehiclewakeupRequestsByID", + "scopes": [] + } + }, + "parameters": [] + }, + "personalizationOnline": { + "id": "personalizationOnline", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": {}, + "parameters": [] + }, + "vehicleLights": { + "id": "vehicleLights", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getLightsStatus": { + "id": "getLightsStatus", + "scopes": [ + "vehicleLights" + ] + } + }, + "parameters": [] + }, + "tripStatistics": { + "id": "tripStatistics", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "deleteTripdataCyclicByID": { + "id": "deleteTripdataCyclicByID", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongtermLast": { + "id": "getTripdataLongtermLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongtermByID": { + "id": "deleteTripdataLongtermByID", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShorttermLast": { + "id": "getTripdataShorttermLast", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataCyclic": { + "id": "getTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataCyclicLast": { + "id": "getTripdataCyclicLast", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongterm": { + "id": "getTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShortterm": { + "id": "deleteTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShortterm": { + "id": "getTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShorttermByID": { + "id": "deleteTripdataShorttermByID", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataCyclic": { + "id": "deleteTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongterm": { + "id": "deleteTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + } + }, + "parameters": [] + }, + "access": { + "id": "access", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getAccessRequestsByID": { + "id": "getAccessRequestsByID", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessUnlock": { + "id": "postAccessUnlock", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessUnlockwithoutspin": { + "id": "postAccessUnlockwithoutspin", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessLock": { + "id": "postAccessLock", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessLockwithoutspin": { + "id": "postAccessLockwithoutspin", + "scopes": [ + "lock_unlock" + ] + } + }, + "parameters": [] + }, + "dealerAppointment": { + "id": "dealerAppointment", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": true, + "endpoint": "vs", + "isEnabled": false, + "status": [ + "DisabledByUser" + ], + "operations": { + "getMaintenanceStatus": { + "id": "getMaintenanceStatus", + "scopes": [ + "serviceInterval" + ] + }, + "getPredictivemaintenanceDealerappointment": { + "id": "getPredictivemaintenanceDealerappointment", + "scopes": [ + "serviceInterval" + ] + }, + "getWarninglightsLast": { + "id": "getWarninglightsLast", + "scopes": [ + "warning_lights" + ] + } + }, + "parameters": [] + }, + "oilLevelStatus": { + "id": "oilLevelStatus", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getStates": { + "id": "getStates", + "scopes": [ + "parkingBrakeStatus", + "ignitionStatus", + "oilLevels" + ] + } + }, + "parameters": [] + } + }, + "parameters": {} +} diff --git a/tests/fixtures/resources/responses/arteon_2023_diesel/last_trip.json b/tests/fixtures/resources/responses/arteon_2023_diesel/last_trip.json new file mode 100644 index 00000000..a570958e --- /dev/null +++ b/tests/fixtures/resources/responses/arteon_2023_diesel/last_trip.json @@ -0,0 +1,14 @@ +{ + "data": { + "id": "2000000000", + "tripEndTimestamp": "2023-11-30T08:53:20Z", + "tripType": "shortTerm", + "vehicleType": "fuel", + "mileage_km": 18, + "startMileage_km": 38933, + "overallMileage_km": 38952, + "travelTime": 27, + "averageFuelConsumption": 7.1, + "averageSpeed_kmph": 41 + } +} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/arteon_2023_diesel/parkingposition.json b/tests/fixtures/resources/responses/arteon_2023_diesel/parkingposition.json new file mode 100644 index 00000000..f3db821f --- /dev/null +++ b/tests/fixtures/resources/responses/arteon_2023_diesel/parkingposition.json @@ -0,0 +1 @@ +{"data":{"lon":8.000000,"lat":52.000000,"carCapturedTimestamp":"2023-11-30T08:54:37Z"}} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/arteon_2023_diesel/selectivestatus_by_app.json b/tests/fixtures/resources/responses/arteon_2023_diesel/selectivestatus_by_app.json new file mode 100644 index 00000000..400f83a2 --- /dev/null +++ b/tests/fixtures/resources/responses/arteon_2023_diesel/selectivestatus_by_app.json @@ -0,0 +1,284 @@ +{ + "access": { + "accessStatus": { + "value": { + "overallStatus": "unsafe", + "carCapturedTimestamp": "2023-11-30T09:44:47Z", + "doors": [ + { + "name": "bonnet", + "status": [ + "open" + ] + }, + { + "name": "frontLeft", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "frontRight", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "rearLeft", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "rearRight", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "trunk", + "status": [ + "unlocked", + "closed" + ] + } + ], + "windows": [ + { + "name": "frontLeft", + "status": [ + "open" + ] + }, + { + "name": "frontRight", + "status": [ + "closed" + ] + }, + { + "name": "rearLeft", + "status": [ + "closed" + ] + }, + { + "name": "rearRight", + "status": [ + "closed" + ] + }, + { + "name": "roofCover", + "status": [ + "unsupported" + ] + }, + { + "name": "sunRoof", + "status": [ + "unsupported" + ] + } + ], + "doorLockStatus": "unlocked" + } + } + }, + "userCapabilities": { + "capabilitiesStatus": { + "value": [ + { + "id": "access", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "cubicNetwork", + "userDisablingAllowed": false + }, + { + "id": "cubicNetworkConsumption", + "userDisablingAllowed": false + }, + { + "id": "dealerAppointment", + "status": [ + 1004 + ], + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": true + }, + { + "id": "destinations", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "digitalKey", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "emergencyCalling", + "expirationDate": "2032-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "fuelStatus", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "honkAndFlash", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "measurements", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "oilLevelStatus", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "parkingPosition", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "personalizationOnline", + "userDisablingAllowed": false + }, + { + "id": "roadsideAssistant", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "state", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryDigitalKey", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryHonkFlash", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryLockUnlock", + "expirationDate": "2025-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "tripStatistics", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealth", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthArchive", + "status": [ + 1007 + ], + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthInspection", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthSettings", + "status": [ + 1007 + ], + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthTrigger", + "status": [ + 1007 + ], + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthWakeUp", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthWarnings", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleLights", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUp", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2052-11-25T10:37:00Z", + "userDisablingAllowed": false + } + ] + } + }, + "fuelStatus": { + "rangeStatus": { + "value": { + "carCapturedTimestamp": "2023-11-30T09:44:47Z", + "carType": "diesel", + "primaryEngine": { + "type": "diesel", + "currentSOC_pct": 19, + "remainingRange_km": 140, + "currentFuelLevel_pct": 19 + }, + "totalRange_km": 140 + } + } + }, + "vehicleLights": { + "lightsStatus": { + "value": { + "carCapturedTimestamp": "2023-11-30T09:44:47Z", + "lights": [ + { + "name": "right", + "status": "off" + }, + { + "name": "left", + "status": "off" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/eup_electric/capabilities.json b/tests/fixtures/resources/responses/eup_electric/capabilities.json new file mode 100644 index 00000000..4147a20a --- /dev/null +++ b/tests/fixtures/resources/responses/eup_electric/capabilities.json @@ -0,0 +1,607 @@ +{ + "vin": "WVWZZZXXXXX000000", + "capabilities": { + "automation": { + "id": "automation", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getChargingTimers": { + "id": "getChargingTimers", + "scopes": [ + "charging" + ] + }, + "getClimatisationTimers": { + "id": "getClimatisationTimers", + "scopes": [ + "climatisation" + ] + }, + "putClimatisationTimers": { + "id": "putClimatisationTimers", + "scopes": [ + "manageClimatisation" + ] + }, + "getDepartureTimers": { + "id": "getDepartureTimers", + "scopes": [ + "climatisation", + "charging" + ] + }, + "postChargingProfiles": { + "id": "postChargingProfiles", + "scopes": [ + "manageCharging" + ] + }, + "putChargingProfiles": { + "id": "putChargingProfiles", + "scopes": [ + "manageCharging" + ] + }, + "putChargingTimers": { + "id": "putChargingTimers", + "scopes": [ + "manageCharging" + ] + }, + "getChargingProfiles": { + "id": "getChargingProfiles", + "scopes": [ + "charging" + ] + }, + "deleteChargingProfilesByID": { + "id": "deleteChargingProfilesByID", + "scopes": [ + "charging" + ] + } + }, + "parameters": [] + }, + "state": { + "id": "state", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getAccessStatus": { + "id": "getAccessStatus", + "scopes": [ + "doors_windows" + ] + } + }, + "parameters": [] + }, + "vehicleWakeUp": { + "id": "vehicleWakeUp", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeup": { + "id": "postVehiclewakeup", + "scopes": [] + }, + "getVehiclewakeupRequestsByID": { + "id": "getVehiclewakeupRequestsByID", + "scopes": [] + } + }, + "parameters": [] + }, + "fuelStatus": { + "id": "fuelStatus", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getFuelStatus": { + "id": "getFuelStatus", + "scopes": [ + "fuelLevels" + ] + } + }, + "parameters": [] + }, + "oilLevelStatus": { + "id": "oilLevelStatus", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getStates": { + "id": "getStates", + "scopes": [ + "parkingBrakeStatus", + "ignitionStatus", + "oilLevels" + ] + } + }, + "parameters": [] + }, + "parkingPosition": { + "id": "parkingPosition", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getParkingposition": { + "id": "getParkingposition", + "scopes": [ + "parking_position" + ] + } + }, + "parameters": [] + }, + "vehicleHealthInspection": { + "id": "vehicleHealthInspection", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMaintenanceStatus": { + "id": "getMaintenanceStatus", + "scopes": [ + "serviceInterval" + ] + } + }, + "parameters": [] + }, + "charging": { + "id": "charging", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getChargingCareStatus": { + "id": "getChargingCareStatus", + "scopes": [ + "manageCharging" + ] + }, + "putChargingMode": { + "id": "putChargingMode", + "scopes": [ + "manageCharging" + ] + }, + "putChargingSettings": { + "id": "putChargingSettings", + "scopes": [ + "manageCharging" + ] + }, + "getChargingStatus": { + "id": "getChargingStatus", + "scopes": [ + "charging" + ] + }, + "postChargingStop": { + "id": "postChargingStop", + "scopes": [ + "manageCharging" + ] + }, + "getChargingMode": { + "id": "getChargingMode", + "scopes": [ + "charging" + ] + }, + "getChargingRequestsByID": { + "id": "getChargingRequestsByID", + "scopes": [ + "manageCharging" + ] + }, + "getChargingSettings": { + "id": "getChargingSettings", + "scopes": [ + "charging" + ] + }, + "postChargingStart": { + "id": "postChargingStart", + "scopes": [ + "manageCharging" + ] + } + }, + "parameters": [ + { + "key": "allowPlugUnlockACPermanent", + "value": "false" + }, + { + "key": "allowPlugUnlockACOnce", + "value": "false" + }, + { + "key": "allowPlugUnlockDCPermanent", + "value": "false" + }, + { + "key": "allowPlugUnlockDCOnce", + "value": "false" + }, + { + "key": "supportsTargetStateOfCharge", + "value": "" + }, + { + "key": "targetStateOfChargeMinimumAllowedValue", + "value": "" + }, + { + "key": "targetStateOfChargeStepSize", + "value": "" + } + ] + }, + "climatisation": { + "id": "climatisation", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getClimatisationSettings": { + "id": "getClimatisationSettings", + "scopes": [ + "climatisation" + ] + }, + "putClimatisationSettings": { + "id": "putClimatisationSettings", + "scopes": [ + "manageClimatisation" + ] + }, + "getClimatisationStatus": { + "id": "getClimatisationStatus", + "scopes": [ + "climatisation" + ] + }, + "postClimatisationStop": { + "id": "postClimatisationStop", + "scopes": [ + "manageClimatisation" + ] + }, + "postWindowheatingStart": { + "id": "postWindowheatingStart", + "scopes": [ + "manageClimatisation" + ] + }, + "getClimatisationRequestsByID": { + "id": "getClimatisationRequestsByID", + "scopes": [ + "manageClimatisation" + ] + }, + "postClimatisationStart": { + "id": "postClimatisationStart", + "scopes": [ + "manageClimatisation" + ] + }, + "getWindowheatingRequestsByID": { + "id": "getWindowheatingRequestsByID", + "scopes": [ + "manageClimatisation" + ] + }, + "postWindowheatingStop": { + "id": "postWindowheatingStop", + "scopes": [ + "manageClimatisation" + ] + } + }, + "parameters": [ + { + "key": "supportsTargetTemperatureInStartClimatisation", + "value": "" + }, + { + "key": "supportsTargetTemperatureInSettings", + "value": "" + }, + { + "key": "supportsClimatisationAtUnlock", + "value": "false" + }, + { + "key": "supportsWindowHeatingEnabled", + "value": "false" + }, + { + "key": "supportsZoneFrontLeftEnabled", + "value": "false" + }, + { + "key": "supportsZoneFrontRightEnabled", + "value": "false" + }, + { + "key": "supportsZoneRearLeftEnabled", + "value": "false" + }, + { + "key": "supportsZoneRearRightEnabled", + "value": "false" + }, + { + "key": "supportsClimatisationMode", + "value": "" + }, + { + "key": "supportsOffGridClimatisation", + "value": "true" + }, + { + "key": "supportsStartParallelClimatisationWindowHeating", + "value": "true" + }, + { + "key": "supportsStartWindowHeating", + "value": "false" + } + ] + }, + "departureProfiles": { + "id": "departureProfiles", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "deleteDepartureProfiles": { + "id": "deleteDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "getDepartureProfiles": { + "id": "getDepartureProfiles", + "scopes": [ + "climatisation", + "charging" + ] + }, + "postDepartureProfiles": { + "id": "postDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "putDepartureProfiles": { + "id": "putDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "deleteDepartureProfilesByID": { + "id": "deleteDepartureProfilesByID", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + } + }, + "parameters": [] + }, + "measurements": { + "id": "measurements", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMeasurements": { + "id": "getMeasurements", + "scopes": [ + "range", + "mileage" + ] + } + }, + "parameters": [] + }, + "hybridCarAuxiliaryHeating": { + "id": "hybridCarAuxiliaryHeating", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [ + "MissingOperation" + ], + "operations": { + "getAuxiliaryheatingRequestsByID": { + "id": "getAuxiliaryheatingRequestsByID", + "scopes": [ + "manageClimatisation" + ] + }, + "postAuxiliaryheatingStart": { + "id": "postAuxiliaryheatingStart", + "scopes": [ + "manageClimatisation" + ] + }, + "postAuxiliaryheatingStop": { + "id": "postAuxiliaryheatingStop", + "scopes": [ + "manageClimatisation" + ] + }, + "putAuxiliaryheatingTimers": { + "id": "putAuxiliaryheatingTimers", + "scopes": [ + "manageClimatisation" + ] + } + }, + "parameters": [ + { + "key": "supportsAutomaticMode", + "value": "false" + } + ] + }, + "tripStatistics": { + "id": "tripStatistics", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "deleteTripdataCyclicByID": { + "id": "deleteTripdataCyclicByID", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongterm": { + "id": "deleteTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongterm": { + "id": "getTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongtermLast": { + "id": "getTripdataLongtermLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShortterm": { + "id": "deleteTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShortterm": { + "id": "getTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataCyclic": { + "id": "getTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataCyclicLast": { + "id": "getTripdataCyclicLast", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShorttermLast": { + "id": "getTripdataShorttermLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShorttermByID": { + "id": "deleteTripdataShorttermByID", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataCyclic": { + "id": "deleteTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongtermByID": { + "id": "deleteTripdataLongtermByID", + "scopes": [ + "tripStatistics" + ] + } + }, + "parameters": [] + }, + "vehicleLights": { + "id": "vehicleLights", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getLightsStatus": { + "id": "getLightsStatus", + "scopes": [ + "vehicleLights" + ] + } + }, + "parameters": [] + }, + "vehicleWakeUpTrigger": { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeupUpdate": { + "id": "postVehiclewakeupUpdate", + "scopes": [] + } + }, + "parameters": [] + } + }, + "parameters": {} +} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/eup_electric/last_trip.json b/tests/fixtures/resources/responses/eup_electric/last_trip.json new file mode 100644 index 00000000..7a56acc8 --- /dev/null +++ b/tests/fixtures/resources/responses/eup_electric/last_trip.json @@ -0,0 +1,16 @@ +{ + "data": { + "id": "2000000000", + "tripEndTimestamp": "2023-12-05T08:27:02Z", + "tripType": "shortTerm", + "vehicleType": "electric", + "mileage_km": 14, + "startMileage_km": 15906, + "overallMileage_km": 15921, + "travelTime": 32, + "averageElectricConsumption": 16.9, + "averageAuxConsumption": 7.6, + "averageRecuperation": 8.1, + "averageSpeed_kmph": 28 + } +} diff --git a/tests/fixtures/resources/responses/eup_electric/parkingposition.json b/tests/fixtures/resources/responses/eup_electric/parkingposition.json new file mode 100644 index 00000000..bdddda9d --- /dev/null +++ b/tests/fixtures/resources/responses/eup_electric/parkingposition.json @@ -0,0 +1 @@ +{"data":{"lon":21.000000,"lat":47.00000,"carCapturedTimestamp":"2023-12-05T07:29:14Z"}} diff --git a/tests/fixtures/resources/responses/eup_electric/selectivestatus_by_app.json b/tests/fixtures/resources/responses/eup_electric/selectivestatus_by_app.json new file mode 100644 index 00000000..d1657e50 --- /dev/null +++ b/tests/fixtures/resources/responses/eup_electric/selectivestatus_by_app.json @@ -0,0 +1,395 @@ +{ + "access": { + "accessStatus": { + "value": { + "overallStatus": "safe", + "carCapturedTimestamp": "2023-12-05T08:29:18Z", + "doors": [ + { + "name": "bonnet", + "status": [ + "closed" + ] + }, + { + "name": "frontLeft", + "status": [ + "locked", + "closed" + ] + }, + { + "name": "frontRight", + "status": [ + "locked", + "closed" + ] + }, + { + "name": "rearLeft", + "status": [ + "locked", + "closed" + ] + }, + { + "name": "rearRight", + "status": [ + "locked", + "closed" + ] + }, + { + "name": "trunk", + "status": [ + "locked", + "closed" + ] + } + ], + "windows": [ + { + "name": "frontLeft", + "status": [ + "unsupported" + ] + }, + { + "name": "frontRight", + "status": [ + "unsupported" + ] + }, + { + "name": "rearLeft", + "status": [ + "unsupported" + ] + }, + { + "name": "rearRight", + "status": [ + "unsupported" + ] + }, + { + "name": "roofCover", + "status": [ + "unsupported" + ] + }, + { + "name": "sunRoof", + "status": [ + "unsupported" + ] + } + ], + "doorLockStatus": "locked" + } + } + }, + "userCapabilities": { + "capabilitiesStatus": { + "value": [ + { + "id": "automation", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "charging", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "climatisation", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "departureProfiles", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "fuelStatus", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "hybridCarAuxiliaryHeating", + "status": [ + 1007 + ], + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "hybridCarAuxiliaryHeatingTimers", + "status": [ + 1007 + ], + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "measurements", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "oilLevelStatus", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "parkingPosition", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "state", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "tripStatistics", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthInspection", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthWakeUp", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleLights", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUp", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2024-10-25T14:11:00Z", + "userDisablingAllowed": false + } + ] + } + }, + "charging": { + "batteryStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:15Z", + "currentSOC_pct": 82, + "cruisingRangeElectric_km": 137 + } + }, + "chargingStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:15Z", + "remainingChargingTimeToComplete_min": 80, + "chargingState": "notReadyForCharging", + "chargeMode": "", + "chargeType": "" + } + }, + "chargingSettings": { + "value": { + "carCapturedTimestamp": "2023-12-04T07:32:11Z", + "maxChargeCurrentAC": "maximum" + } + }, + "plugStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:42Z", + "plugConnectionState": "disconnected", + "plugLockState": "locked", + "externalPower": "unavailable", + "ledColor": "none" + } + }, + "chargeMode": { + "error": { + "message": "Bad Gateway", + "errorTimeStamp": "2023-12-05T08:30:40Z", + "info": "Upstream service responded with an unexpected status. If the problem persists, please contact our support.", + "code": 4111, + "group": 2, + "retry": true + } + } + }, + "climatisation": { + "climatisationSettings": { + "value": { + "carCapturedTimestamp": "2023-11-24T13:10:27Z", + "targetTemperature_C": 30, + "targetTemperature_F": 88, + "climatisationWithoutExternalPower": true + } + }, + "climatisationStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:10Z", + "climatisationState": "off" + } + }, + "windowHeatingStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:11Z", + "windowHeatingStatus": [ + { + "windowLocation": "front", + "windowHeatingState": "off" + }, + { + "windowLocation": "rear", + "windowHeatingState": "off" + } + ] + } + } + }, + "fuelStatus": { + "rangeStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:27:15Z", + "carType": "electric", + "primaryEngine": { + "type": "electric", + "currentSOC_pct": 82, + "remainingRange_km": 137 + }, + "totalRange_km": 137 + } + } + }, + "vehicleLights": { + "lightsStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T08:29:18Z", + "lights": [ + { + "name": "right", + "status": "off" + }, + { + "name": "left", + "status": "off" + } + ] + } + } + }, + "departureProfiles": { + "departureProfilesStatus": { + "value": { + "carCapturedTimestamp": "2023-11-27T23:42:03Z", + "minSOC_pct": 30, + "timers": [ + { + "id": 2, + "enabled": false, + "recurringTimer": { + "startTime": "06:00", + "recurringOn": { + "mondays": true, + "tuesdays": true, + "wednesdays": true, + "thursdays": true, + "fridays": true, + "saturdays": true, + "sundays": true + } + }, + "profileIDs": [ + 1 + ] + }, + { + "id": 1, + "enabled": false, + "recurringTimer": { + "startTime": "16:00", + "recurringOn": { + "mondays": true, + "tuesdays": true, + "wednesdays": true, + "thursdays": true, + "fridays": true, + "saturdays": true, + "sundays": true + } + }, + "profileIDs": [ + 2 + ] + }, + { + "id": 3, + "enabled": false, + "recurringTimer": { + "startTime": "13:00", + "recurringOn": { + "mondays": true, + "tuesdays": true, + "wednesdays": true, + "thursdays": true, + "fridays": true, + "saturdays": true, + "sundays": true + } + }, + "profileIDs": [ + 2 + ] + } + ], + "profiles": [ + { + "id": 1, + "name": "Profile 1", + "charging": true, + "climatisation": false, + "targetSOC_pct": 90, + "maxChargeCurrentAC": 5, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "00:00", + "endTime": "00:00" + } + ] + }, + { + "id": 2, + "name": "Profile 2", + "charging": true, + "climatisation": true, + "targetSOC_pct": 90, + "maxChargeCurrentAC": 32, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "00:00", + "endTime": "00:00" + } + ] + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/golf_gte_hybrid/capabilities.json b/tests/fixtures/resources/responses/golf_gte_hybrid/capabilities.json new file mode 100644 index 00000000..4f97b415 --- /dev/null +++ b/tests/fixtures/resources/responses/golf_gte_hybrid/capabilities.json @@ -0,0 +1,791 @@ +{ + "vin": "WVWZZZXXXXX000000", + "capabilities": { + "climatisation": { + "id": "climatisation", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getClimatisationSettings": { + "id": "getClimatisationSettings", + "scopes": [ + "climatisation" + ] + }, + "getClimatisationStatus": { + "id": "getClimatisationStatus", + "scopes": [ + "climatisation" + ] + }, + "postWindowheatingStart": { + "id": "postWindowheatingStart", + "scopes": [ + "manageClimatisation" + ] + }, + "postWindowheatingStop": { + "id": "postWindowheatingStop", + "scopes": [ + "manageClimatisation" + ] + }, + "getClimatisationRequestsByID": { + "id": "getClimatisationRequestsByID", + "scopes": [ + "manageClimatisation" + ] + }, + "putClimatisationSettings": { + "id": "putClimatisationSettings", + "scopes": [ + "manageClimatisation" + ] + }, + "postClimatisationStart": { + "id": "postClimatisationStart", + "scopes": [ + "manageClimatisation" + ] + }, + "postClimatisationStop": { + "id": "postClimatisationStop", + "scopes": [ + "manageClimatisation" + ] + }, + "getWindowheatingRequestsByID": { + "id": "getWindowheatingRequestsByID", + "scopes": [ + "manageClimatisation" + ] + } + }, + "parameters": [ + { + "key": "supportsTargetTemperatureInStartClimatisation", + "value": "" + }, + { + "key": "supportsTargetTemperatureInSettings", + "value": "" + }, + { + "key": "supportsClimatisationAtUnlock", + "value": "false" + }, + { + "key": "supportsWindowHeatingEnabled", + "value": "false" + }, + { + "key": "supportsZoneFrontLeftEnabled", + "value": "false" + }, + { + "key": "supportsZoneFrontRightEnabled", + "value": "false" + }, + { + "key": "supportsZoneRearLeftEnabled", + "value": "false" + }, + { + "key": "supportsZoneRearRightEnabled", + "value": "false" + }, + { + "key": "supportsClimatisationMode", + "value": "" + }, + { + "key": "supportsOffGridClimatisation", + "value": "true" + }, + { + "key": "supportsStartParallelClimatisationWindowHeating", + "value": "true" + }, + { + "key": "supportsStartWindowHeating", + "value": "true" + } + ] + }, + "access": { + "id": "access", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postAccessLock": { + "id": "postAccessLock", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessLockwithoutspin": { + "id": "postAccessLockwithoutspin", + "scopes": [ + "lock_unlock" + ] + }, + "getAccessRequestsByID": { + "id": "getAccessRequestsByID", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessUnlock": { + "id": "postAccessUnlock", + "scopes": [ + "lock_unlock" + ] + }, + "postAccessUnlockwithoutspin": { + "id": "postAccessUnlockwithoutspin", + "scopes": [ + "lock_unlock" + ] + } + }, + "parameters": [] + }, + "departureProfiles": { + "id": "departureProfiles", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "deleteDepartureProfiles": { + "id": "deleteDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "getDepartureProfiles": { + "id": "getDepartureProfiles", + "scopes": [ + "climatisation", + "charging" + ] + }, + "postDepartureProfiles": { + "id": "postDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "putDepartureProfiles": { + "id": "putDepartureProfiles", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + }, + "deleteDepartureProfilesByID": { + "id": "deleteDepartureProfilesByID", + "scopes": [ + "manageClimatisation", + "manageCharging" + ] + } + }, + "parameters": [] + }, + "parkingInformation": { + "id": "parkingInformation", + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": false, + "status": [ + "MissingLicense", + "LicenseExpired" + ], + "operations": {}, + "parameters": [] + }, + "parkingPosition": { + "id": "parkingPosition", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getParkingposition": { + "id": "getParkingposition", + "scopes": [ + "parking_position" + ] + } + }, + "parameters": [] + }, + "honkAndFlash": { + "id": "honkAndFlash", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postHonkandflash": { + "id": "postHonkandflash", + "scopes": [ + "honk" + ] + }, + "getHonkandflashRequestsByID": { + "id": "getHonkandflashRequestsByID", + "scopes": [ + "honk" + ] + } + }, + "parameters": [] + }, + "measurements": { + "id": "measurements", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMeasurements": { + "id": "getMeasurements", + "scopes": [ + "range", + "mileage" + ] + } + }, + "parameters": [] + }, + "automation": { + "id": "automation", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getChargingProfiles": { + "id": "getChargingProfiles", + "scopes": [ + "charging" + ] + }, + "putChargingProfiles": { + "id": "putChargingProfiles", + "scopes": [ + "manageCharging" + ] + }, + "deleteChargingProfilesByID": { + "id": "deleteChargingProfilesByID", + "scopes": [ + "charging" + ] + }, + "getChargingTimers": { + "id": "getChargingTimers", + "scopes": [ + "charging" + ] + }, + "getClimatisationTimers": { + "id": "getClimatisationTimers", + "scopes": [ + "climatisation" + ] + }, + "getDepartureTimers": { + "id": "getDepartureTimers", + "scopes": [ + "climatisation", + "charging" + ] + }, + "postChargingProfiles": { + "id": "postChargingProfiles", + "scopes": [ + "manageCharging" + ] + }, + "putChargingTimers": { + "id": "putChargingTimers", + "scopes": [ + "manageCharging" + ] + }, + "putClimatisationTimers": { + "id": "putClimatisationTimers", + "scopes": [ + "manageClimatisation" + ] + } + }, + "parameters": [] + }, + "charging": { + "id": "charging", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "putChargingMode": { + "id": "putChargingMode", + "scopes": [ + "manageCharging" + ] + }, + "getChargingSettings": { + "id": "getChargingSettings", + "scopes": [ + "charging" + ] + }, + "postChargingStop": { + "id": "postChargingStop", + "scopes": [ + "manageCharging" + ] + }, + "getChargingCareStatus": { + "id": "getChargingCareStatus", + "scopes": [ + "manageCharging" + ] + }, + "getChargingMode": { + "id": "getChargingMode", + "scopes": [ + "charging" + ] + }, + "getChargingRequestsByID": { + "id": "getChargingRequestsByID", + "scopes": [ + "manageCharging" + ] + }, + "putChargingSettings": { + "id": "putChargingSettings", + "scopes": [ + "manageCharging" + ] + }, + "postChargingStart": { + "id": "postChargingStart", + "scopes": [ + "manageCharging" + ] + }, + "getChargingStatus": { + "id": "getChargingStatus", + "scopes": [ + "charging" + ] + } + }, + "parameters": [ + { + "key": "allowPlugUnlockACPermanent", + "value": "false" + }, + { + "key": "allowPlugUnlockACOnce", + "value": "false" + }, + { + "key": "allowPlugUnlockDCPermanent", + "value": "false" + }, + { + "key": "allowPlugUnlockDCOnce", + "value": "false" + }, + { + "key": "supportsTargetStateOfCharge", + "value": "" + }, + { + "key": "targetStateOfChargeMinimumAllowedValue", + "value": "" + }, + { + "key": "targetStateOfChargeStepSize", + "value": "" + } + ] + }, + "dealerAppointment": { + "id": "dealerAppointment", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": true, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMaintenanceStatus": { + "id": "getMaintenanceStatus", + "scopes": [ + "serviceInterval" + ] + }, + "getPredictivemaintenanceDealerappointment": { + "id": "getPredictivemaintenanceDealerappointment", + "scopes": [ + "serviceInterval" + ] + }, + "getWarninglightsLast": { + "id": "getWarninglightsLast", + "scopes": [ + "warning_lights" + ] + } + }, + "parameters": [] + }, + "destinations": { + "id": "destinations", + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": false, + "status": [ + "MissingLicense", + "LicenseExpired" + ], + "operations": { + "deleteDestinationsByID": { + "id": "deleteDestinationsByID", + "scopes": [ + "navigation" + ] + }, + "getDestinations": { + "id": "getDestinations", + "scopes": [ + "navigation" + ] + }, + "postDestinations": { + "id": "postDestinations", + "scopes": [ + "navigation" + ] + }, + "putDestinations": { + "id": "putDestinations", + "scopes": [ + "navigation" + ] + } + }, + "parameters": [ + { + "key": "chargingStationsForEVTourImport", + "value": "" + } + ] + }, + "fuelStatus": { + "id": "fuelStatus", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getFuelStatus": { + "id": "getFuelStatus", + "scopes": [ + "fuelLevels" + ] + } + }, + "parameters": [] + }, + "hybridCarAuxiliaryHeating": { + "id": "hybridCarAuxiliaryHeating", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getAuxiliaryheatingRequestsByID": { + "id": "getAuxiliaryheatingRequestsByID", + "scopes": [ + "manageClimatisation" + ] + }, + "postAuxiliaryheatingStart": { + "id": "postAuxiliaryheatingStart", + "scopes": [ + "manageClimatisation" + ] + }, + "postAuxiliaryheatingStop": { + "id": "postAuxiliaryheatingStop", + "scopes": [ + "manageClimatisation" + ] + }, + "putAuxiliaryheatingTimers": { + "id": "putAuxiliaryheatingTimers", + "scopes": [ + "manageClimatisation" + ] + } + }, + "parameters": [ + { + "key": "supportsAutomaticMode", + "value": "false" + } + ] + }, + "oilLevelStatus": { + "id": "oilLevelStatus", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getStates": { + "id": "getStates", + "scopes": [ + "parkingBrakeStatus", + "ignitionStatus", + "oilLevels" + ] + } + }, + "parameters": [] + }, + "roadsideAssistant": { + "id": "roadsideAssistant", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": {}, + "parameters": [] + }, + "vehicleHealthInspection": { + "id": "vehicleHealthInspection", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getMaintenanceStatus": { + "id": "getMaintenanceStatus", + "scopes": [ + "serviceInterval" + ] + } + }, + "parameters": [] + }, + "vehicleWakeUp": { + "id": "vehicleWakeUp", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeup": { + "id": "postVehiclewakeup", + "scopes": [] + }, + "getVehiclewakeupRequestsByID": { + "id": "getVehiclewakeupRequestsByID", + "scopes": [] + } + }, + "parameters": [] + }, + "vehicleWakeUpTrigger": { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "postVehiclewakeupUpdate": { + "id": "postVehiclewakeupUpdate", + "scopes": [] + } + }, + "parameters": [] + }, + "state": { + "id": "state", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getAccessStatus": { + "id": "getAccessStatus", + "scopes": [ + "doors_windows" + ] + } + }, + "parameters": [] + }, + "vehicleHealthWarnings": { + "id": "vehicleHealthWarnings", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getWarninglightsLast": { + "id": "getWarninglightsLast", + "scopes": [ + "warning_lights" + ] + } + }, + "parameters": [] + }, + "vehicleHealth": { + "id": "vehicleHealth", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": {}, + "parameters": [] + }, + "vehicleLights": { + "id": "vehicleLights", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getLightsStatus": { + "id": "getLightsStatus", + "scopes": [ + "vehicleLights" + ] + } + }, + "parameters": [] + }, + "tripStatistics": { + "id": "tripStatistics", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false, + "endpoint": "vs", + "isEnabled": true, + "status": [], + "operations": { + "getTripdataCyclic": { + "id": "getTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongtermLast": { + "id": "getTripdataLongtermLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongtermByID": { + "id": "deleteTripdataLongtermByID", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShortterm": { + "id": "getTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataShorttermLast": { + "id": "getTripdataShorttermLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShorttermByID": { + "id": "deleteTripdataShorttermByID", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataCyclic": { + "id": "deleteTripdataCyclic", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataCyclicLast": { + "id": "getTripdataCyclicLast", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataCyclicByID": { + "id": "deleteTripdataCyclicByID", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataLongterm": { + "id": "deleteTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + }, + "getTripdataLongterm": { + "id": "getTripdataLongterm", + "scopes": [ + "tripStatistics" + ] + }, + "deleteTripdataShortterm": { + "id": "deleteTripdataShortterm", + "scopes": [ + "tripStatistics" + ] + } + }, + "parameters": [] + } + }, + "parameters": {} +} diff --git a/tests/fixtures/resources/responses/golf_gte_hybrid/last_trip.json b/tests/fixtures/resources/responses/golf_gte_hybrid/last_trip.json new file mode 100644 index 00000000..2023791a --- /dev/null +++ b/tests/fixtures/resources/responses/golf_gte_hybrid/last_trip.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": "2000000000", + "tripEndTimestamp": "2023-12-04T16:10:01Z", + "tripType": "shortTerm", + "vehicleType": "hybrid", + "mileage_km": 7, + "startMileage_km": 32199, + "overallMileage_km": 32205, + "travelTime": 10, + "averageFuelConsumption": 0, + "averageElectricConsumption": 28.9, + "averageSpeed_kmph": 40 + } +} \ No newline at end of file diff --git a/tests/fixtures/resources/responses/golf_gte_hybrid/selectivestatus_by_app.json b/tests/fixtures/resources/responses/golf_gte_hybrid/selectivestatus_by_app.json new file mode 100644 index 00000000..639f14cf --- /dev/null +++ b/tests/fixtures/resources/responses/golf_gte_hybrid/selectivestatus_by_app.json @@ -0,0 +1,557 @@ +{ + "access": { + "accessStatus": { + "value": { + "overallStatus": "unsafe", + "carCapturedTimestamp": "2023-12-05T06:41:03Z", + "doors": [ + { + "name": "bonnet", + "status": [ + "closed" + ] + }, + { + "name": "frontLeft", + "status": [ + "unlocked", + "open" + ] + }, + { + "name": "frontRight", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "rearLeft", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "rearRight", + "status": [ + "unlocked", + "closed" + ] + }, + { + "name": "trunk", + "status": [ + "unlocked", + "closed" + ] + } + ], + "windows": [ + { + "name": "frontLeft", + "status": [ + "closed" + ] + }, + { + "name": "frontRight", + "status": [ + "closed" + ] + }, + { + "name": "rearLeft", + "status": [ + "closed" + ] + }, + { + "name": "rearRight", + "status": [ + "closed" + ] + }, + { + "name": "roofCover", + "status": [ + "unsupported" + ] + }, + { + "name": "sunRoof", + "status": [ + "unsupported" + ] + } + ], + "doorLockStatus": "unlocked" + } + } + }, + "userCapabilities": { + "capabilitiesStatus": { + "value": [ + { + "id": "access", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "automation", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "charging", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "climatisation", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "dealerAppointment", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": true + }, + { + "id": "departureProfiles", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "destinations", + "status": [ + 2003, + 2002 + ], + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "destinationsTours", + "status": [ + 2003, + 2002 + ], + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "emergencyCalling", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "fuelStatus", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "honkAndFlash", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "hybridCarAuxiliaryHeating", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "hybridCarAuxiliaryHeatingTimers", + "status": [ + 1007 + ], + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "measurements", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "news", + "status": [ + 2003, + 2002 + ], + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "oilLevelStatus", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "parkingInformation", + "status": [ + 2003, + 2002 + ], + "expirationDate": "2023-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "parkingPosition", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "roadsideAssistant", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "state", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "theftWarning", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryAntiTheftAlert", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryAntiTheftAlertDelete", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryHonkFlash", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "transactionHistoryLockUnlock", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "tripStatistics", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealth", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthArchive", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthInspection", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthSettings", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthTrigger", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthWakeUp", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleHealthWarnings", + "expirationDate": "2030-03-13T10:16:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleLights", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUp", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + }, + { + "id": "vehicleWakeUpTrigger", + "expirationDate": "2025-03-14T12:50:00Z", + "userDisablingAllowed": false + } + ] + } + }, + "charging": { + "batteryStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:15Z", + "currentSOC_pct": 65, + "cruisingRangeElectric_km": 14 + } + }, + "chargingStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:15Z", + "chargingState": "notReadyForCharging", + "chargeMode": "", + "chargeType": "" + } + }, + "chargingSettings": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:40:51Z", + "maxChargeCurrentAC": "reduced" + } + }, + "plugStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:15Z", + "plugConnectionState": "disconnected", + "plugLockState": "unlocked", + "externalPower": "unavailable", + "ledColor": "none" + } + }, + "chargeMode": { + "error": { + "message": "Bad Gateway", + "errorTimeStamp": "2023-12-05T08:25:12Z", + "info": "Upstream service responded with an unexpected status. If the problem persists, please contact our support.", + "code": 4111, + "group": 2, + "retry": true + } + } + }, + "climatisation": { + "climatisationSettings": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:40:48Z", + "targetTemperature_C": 22, + "targetTemperature_F": 72, + "climatisationWithoutExternalPower": true, + "heaterSource": "electric" + } + }, + "climatisationStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:12Z", + "climatisationState": "off" + } + }, + "windowHeatingStatus": { + "value": { + "carCapturedTimestamp": "2023-12-03T10:30:14Z", + "windowHeatingStatus": [ + { + "windowLocation": "front", + "windowHeatingState": "off" + }, + { + "windowLocation": "rear", + "windowHeatingState": "off" + } + ] + } + } + }, + "fuelStatus": { + "rangeStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:15Z", + "carType": "hybrid", + "primaryEngine": { + "type": "gasoline", + "currentSOC_pct": 37, + "remainingRange_km": 180, + "currentFuelLevel_pct": 37 + }, + "secondaryEngine": { + "type": "electric", + "currentSOC_pct": 65, + "remainingRange_km": 14 + }, + "totalRange_km": 194 + } + } + }, + "vehicleLights": { + "lightsStatus": { + "value": { + "carCapturedTimestamp": "2023-12-05T06:41:03Z", + "lights": [ + { + "name": "right", + "status": "on" + }, + { + "name": "left", + "status": "on" + } + ] + } + } + }, + "departureProfiles": { + "departureProfilesStatus": { + "value": { + "carCapturedTimestamp": "2023-12-04T16:01:44Z", + "minSOC_pct": 0, + "timers": [ + { + "id": 3, + "enabled": true, + "recurringTimer": { + "startTime": "15:30", + "recurringOn": { + "mondays": true, + "tuesdays": true, + "wednesdays": true, + "thursdays": true, + "fridays": false, + "saturdays": false, + "sundays": false + } + }, + "profileIDs": [ + 3 + ] + }, + { + "id": 2, + "enabled": true, + "recurringTimer": { + "startTime": "11:00", + "recurringOn": { + "mondays": true, + "tuesdays": true, + "wednesdays": true, + "thursdays": true, + "fridays": false, + "saturdays": false, + "sundays": false + } + }, + "profileIDs": [ + 4 + ] + }, + { + "id": 1, + "enabled": true, + "recurringTimer": { + "startTime": "11:00", + "recurringOn": { + "mondays": false, + "tuesdays": false, + "wednesdays": false, + "thursdays": false, + "fridays": true, + "saturdays": false, + "sundays": false + } + }, + "profileIDs": [ + 5 + ] + } + ], + "profiles": [ + { + "id": 1, + "name": "Profile 1", + "charging": true, + "climatisation": false, + "targetSOC_pct": 100, + "maxChargeCurrentAC": 16, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "22:00", + "endTime": "22:00" + } + ] + }, + { + "id": 2, + "name": "Profile 2", + "charging": true, + "climatisation": true, + "targetSOC_pct": 100, + "maxChargeCurrentAC": 10, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "01:00", + "endTime": "01:00" + } + ] + }, + { + "id": 3, + "name": "Profile 3", + "charging": true, + "climatisation": false, + "targetSOC_pct": 100, + "maxChargeCurrentAC": 5, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "00:00", + "endTime": "00:00" + } + ] + }, + { + "id": 4, + "name": "Profile 4", + "charging": true, + "climatisation": false, + "targetSOC_pct": 90, + "maxChargeCurrentAC": 5, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "00:00", + "endTime": "00:00" + } + ] + }, + { + "id": 5, + "name": "Profile 5", + "charging": true, + "climatisation": false, + "targetSOC_pct": 100, + "maxChargeCurrentAC": 32, + "preferredChargingTimes": [ + { + "id": 1, + "enabled": false, + "startTime": "22:00", + "endTime": "22:00" + } + ] + } + ] + } + } + } +} \ No newline at end of file From cf7e10f7f3911642556ca10e1a3b562709604e03 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:13:04 +0100 Subject: [PATCH 20/41] Fix Re-Login race condition (#223) --- volkswagencarnet/vw_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 5a27763e..c8199a4d 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -101,7 +101,6 @@ async def doLogin(self, tries: int = 1): _LOGGER.info("Successfully logged in") self._session_tokens["identity"] = self._session_tokens["Legacy"].copy() - self._session_logged_in = True # Get list of vehicles from account _LOGGER.debug("Fetching vehicles associated with account") @@ -348,6 +347,7 @@ def base64URLEncode(s): _LOGGER.warning("User identity token could not be verified!") else: _LOGGER.debug("User identity token verified OK.") + self._session_logged_in = True except Exception as error: _LOGGER.error(f"Login failed for {BRAND} account, {error}") _LOGGER.exception(error) From 3ea6ae048b0cfc51c7438deb214c9f2e7ca99b47 Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Thu, 7 Dec 2023 20:37:39 +0100 Subject: [PATCH 21/41] removed accidentially commited files --- rate_limit_analysis.py | 63 ------------------------------------------ refresh_test.py | 34 ----------------------- 2 files changed, 97 deletions(-) delete mode 100644 rate_limit_analysis.py delete mode 100644 refresh_test.py diff --git a/rate_limit_analysis.py b/rate_limit_analysis.py deleted file mode 100644 index 6971dfa0..00000000 --- a/rate_limit_analysis.py +++ /dev/null @@ -1,63 +0,0 @@ -from volkswagencarnet.vw_connection import Connection -import volkswagencarnet.vw_const as const -from tests.credentials import username, password - -from aiohttp import ClientSession -import pprint -import asyncio -import logging - -logging.basicConfig(level=logging.DEBUG) - -VW_USERNAME=username -VW_PASSWORD=password - -SERVICES = ["access", - "fuelStatus", - "vehicleLights", - "vehicleHealthInspection", - "measurements", - "charging", - "climatisation", - "automation"] - -async def main(): - """Main method.""" - async with ClientSession(headers={'Connection': 'keep-alive'}) as session: - connection = Connection(session, VW_USERNAME, VW_PASSWORD) - request_count = 0 - if await connection.doLogin(): - logging.info(f"Logged in to account {VW_USERNAME}") - logging.info("Tokens:") - logging.info(pprint.pformat(connection._session_tokens)) - - vehicle = connection.vehicles[0].vin - error_count = 0 - while True: - await connection.validate_tokens - #response = await connection.get( - # f"{const.BASE_API}/vehicle/v1/vehicles/{vehicle}/selectivestatus?jobs={','.join(SERVICES)}", "" - #)ng - response = await connection.get( - f"{const.BASE_API}/vehicle/v1/trips/{vehicle}/shortterm/last", "" - ) - - - request_count += 1 - logging.info(f"Request count is {request_count} with {len(SERVICES)} services, response: {pprint.pformat(response)[1:100]}") - if "status_code" in response: - if response["status_code"] != 403: - logging.error(f"Something went wrong, received status code {response.get('status_code')}, bailing out") - exit(-1) - error_count += 1 - if error_count > 3: - logging.error("More than 3 errors in a row, bailing out") - else: - error_count = 0 - - await asyncio.sleep(1) - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - # loop.run(main()) - loop.run_until_complete(main()) \ No newline at end of file diff --git a/refresh_test.py b/refresh_test.py deleted file mode 100644 index 756dbe1b..00000000 --- a/refresh_test.py +++ /dev/null @@ -1,34 +0,0 @@ -from volkswagencarnet.vw_connection import Connection -import volkswagencarnet.vw_const as const -from tests.credentials import username, password - -from aiohttp import ClientSession -import pprint -import asyncio -import logging - -logging.basicConfig(level=logging.DEBUG) - -VW_USERNAME=username -VW_PASSWORD=password - - -async def main(): - """Main method.""" - async with ClientSession(headers={'Connection': 'keep-alive'}) as session: - connection = Connection(session, VW_USERNAME, VW_PASSWORD) - request_count = 0 - connection._session_logged_in = True - connection._session_tokens["Legacy"] = dict() - connection._session_tokens["Legacy"]["access_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwNjNkODdkMS1lNTc4LTRiNDMtOTEzNC1kY2YyNzVlNWQxOTAiLCJhdWQiOiJhMjRmYmE2My0zNGIzLTRkNDMtYjE4MS05NDIxMTFlNmJkYThAYXBwc192dy1kaWxhYl9jb20iLCJzY3AiOiJvcGVuaWQgcHJvZmlsZSBiYWRnZSBjYXJzIGRlYWxlcnMgdmluIiwiYWF0IjoiaWRlbnRpdHlraXQiLCJpc3MiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8iLCJqdHQiOiJhY2Nlc3NfdG9rZW4iLCJleHAiOjE3MDE3NTkwNTEsImlhdCI6MTcwMTc1NTQ1MSwibGVlIjpbIlZPTEtTV0FHRU4iXSwianRpIjoiZDJiMGEwN2QtMjJlMS00ZjY3LTk3MWEtNjJhNjIzMDU1YjE5In0.VGwJoHlxy3FUqLIWGh82Lo0yxsJXf9gRBMDhqf8BqE4-de_myCzBdgwXVUgHMaDWIlvVArEXlGQhnvse7JArqgiYt0bjfaXhlZh8x2F8R9HcoLmKgqHoUJRyuUSq5LZyvzKgrw9k7FDmqgCRju8vZhvb3wRcmVZ89tirI9xvoyF65nCkMg_10nhRcEfu_JBf-8xNEZdMtgFg4zVCXYnckd9KrhCLmRvBc1bsTuaLnxrU56DD_yORMhmHBI39SXh0ME1bfL7Td2aadvIJJovA0KoEVdrKUOyCrmxb87Kam19OR36Um1zVe0GVGj4LPxf-NtQwSevBOQ4Px0Ti0iFvKORct4gj2bhKW8wOX7r8llLmALcCNToNYhUJSFAkqgMJWWk9fRu7Acn-RAcIF20tIe9KdO5yq3gkanqB1n2RoSNmoRl-C0ltjtdsc0peOz3U3wjxwFA3nSz7RndfY21rOTOfCq9yiFT0XO65dQ_w3_yH3UoMAArxYwR_0mJr4pC6EYmr92EHkn8WR3n9P6NAw0fFMzPNLoCszOCLNZvQND3abLmO4f0kw4mjJK9vZTFI2_JjAEKNE7hqXW_c9YzngTvltNLsHx67xZHql3ywMZvIYCrO_UfKA65NLdfmarYo32E0-EjonozwPaJPWGM-tKvwqlXJjHPpLRdHOU3SGNs' - connection._session_tokens["Legacy"]["refresh_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwNjNkODdkMS1lNTc4LTRiNDMtOTEzNC1kY2YyNzVlNWQxOTAiLCJhdWQiOiJhMjRmYmE2My0zNGIzLTRkNDMtYjE4MS05NDIxMTFlNmJkYThAYXBwc192dy1kaWxhYl9jb20iLCJhY3IiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8vYXNzdXJhbmNlL2xvYS0yIiwic2NwIjoib3BlbmlkIHByb2ZpbGUgYmFkZ2UgY2FycyBkZWFsZXJzIHZpbiIsImFhdCI6ImlkZW50aXR5a2l0IiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS52d2dyb3VwLmlvIiwianR0IjoicmVmcmVzaF90b2tlbiIsImV4cCI6MTcxNzMwNzQ1MSwiaWF0IjoxNzAxNzU1NDUxLCJqdGkiOiJiMzEyM2IyMy04MDE0LTQ2MmMtYjEzNC01Mzk0ODFlNjQ1N2QifQ.K3Pb4wzqRAz9UIDtPFm8HA2iVNzBWf5ZL2_9U5tKwF1eawd8j3j8I1OBqhBVXuF6wAHlCckpDb99o6JVEC2KsdfJFMOkVj_84s5UKyHCXL-PZc0W-TSslaR9V0dH0TkAgffC3eP2wFlQOwjGKwxj1ljT_2i66oo-hj8TcjSAjeTAIcAu-ySWDL4MFb4vo4omdtAAXzOq677p9JTkP2CSpkkjELrmyjhXb-2zEQcLEZxLEy5eD01YwzhDVLPV_Kti2nYPjSASfgd9pTE0rkIpQdCSQJKpWF0XqYIlRM3RbNekeVTDxD4GK77X3LlEb_VXfuNHm9ETS02LiV2C9Qt79ol4MKVv6Ij9fF0fuAsVXbz7Ft3AH413bjy-rhuP6y8fY-wTf2MCplljmk4U9D2m0mZeAfKnRqv6Z_mUrpY-0bB9gN9pq9T5Mm7f29i1qZYDS5XDwar2muH50LwL3jQj6rtCVImTXpxN8Jlh7cuBIx3IRHjqskjBf0P_SwFhUNFkSzQPj56ib3Lle6HmzaosmWaWU9fyO2OR4rBR2PLfdQrDudTFeLKYwUUQEXbz5OwhsZt6G0woKYnpYneYDyYTewHvsltsnbGsn00bzEgob3552QK-IKIEYDS8ZwVCDkhhKfB66kyruvEBeP2dAEopDgNYR2jcxWtYlM0Tz6OuAVs' - connection._session_tokens["Legacy"]["id_token"] = 'eyJraWQiOiI0ODEyODgzZi05Y2FiLTQwMWMtYTI5OC0wZmEyMTA5Y2ViY2EiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoibjNTRTY5b1Jac1VLaFZITzE1NHlzQSIsInN1YiI6IjA2M2Q4N2QxLWU1NzgtNGI0My05MTM0LWRjZjI3NWU1ZDE5MCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2lkZW50aXR5LnZ3Z3JvdXAuaW8iLCJqdHQiOiJpZF90b2tlbiIsImxlZSI6WyJWT0xLU1dBR0VOIl0sImF1ZCI6ImEyNGZiYTYzLTM0YjMtNGQ0My1iMTgxLTk0MjExMWU2YmRhOEBhcHBzX3Z3LWRpbGFiX2NvbSIsImFjciI6Imh0dHBzOi8vaWRlbnRpdHkudndncm91cC5pby9hc3N1cmFuY2UvbG9hLTIiLCJ1cGRhdGVkX2F0IjoxNjc2NDc5MzMxMDMzLCJhYXQiOiJpZGVudGl0eWtpdCIsImV4cCI6MTcwMTc1OTA1MSwiaWF0IjoxNzAxNzU1NDUxLCJqdGkiOiI2ZGQxYWE1Yy0wZWU4LTRlYTktOTY5Yi1jZjA5N2ZjOGViYzUiLCJlbWFpbCI6Im9saXZlckByYWhuZXIubWUifQ.d_3ySV9qle_rN-9wKhzEAa-RuHn0exsdzv7Ilrg2d8dcPfa7alzYAauIH-ZAM86x7O_F8G3fuTMnFK9tjCmhhhCQbvMfSuCQsId0rlyJIufNEwQZkzHXqYx2e8_zSRCDIZXD_uE3KHfe0vSKXEgfB2ijchgMdclkjF0W6TaDVBiaqKg9SX6IvSGcWS__aNcKBsi177GHq6WPDtuvaOdjFjS-AzwGq_ktcFvNpkkIy2Vfu3ZsYtetcupRQ03njKIX_c0O3_OBAducvue0YhuDeU-0gXZwtymyiDjcjYe8eLzNMMYGe9gU-IR10BZjWI7FM7QeRShg8vuNGAzxR4nyonhBct6KSKS66a0aIIQ2hqJYxhBe9yKpkCz5nlUkrY1r0TtVW1OfLYXwVaFaGdD_9rc4UVMMRVsA1jRK2REg1EyzT-mto0MfsTZG9Os49_wPYY4hBqwAXuHRFSvC3-Zt56IL--N2u9VCJU32l9t9rSL0iWnV8sVGZ2r800U4CWa42ycvT-CbPUGvQCMxtY2q3DltZe1D6yeKkxs5N0S24qc2TYuvEztwWCiE_bkd9ZPJeOAdTX3OORsgIeOjtdweyKBJ-zswjyj4c3JIAimwtu1b9sQnMzzqZdevtWkd3Ogh0VLX6uWaVzInHvmH96DePlcA1zm8PWnvgBdYAJ_78pQ' - connection._session_tokens["identity"] = connection._session_tokens["Legacy"].copy() - - await connection.validate_tokens - - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - # loop.run(main()) - loop.run_until_complete(main()) \ No newline at end of file From f83bc95460043f0353e419733ad47672a80c59af Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Fri, 8 Dec 2023 11:06:47 +0100 Subject: [PATCH 22/41] fix 204 handling --- volkswagencarnet/vw_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index c8199a4d..ba24f1e2 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -402,7 +402,7 @@ async def _request(self, method, url, **kwargs): try: if response.status == 204: - res = {"status_code": response.status, "body": response.text} + res = {"status_code": response.status} elif response.status >= 200 or response.status <= 300: res = await response.json(loads=json_loads) else: From ff718f15c1a8c61b21c118fc6ed7e98967989d22 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:09:43 +0100 Subject: [PATCH 23/41] Fix parking time error (#227) --- volkswagencarnet/vw_vehicle.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index acf589ad..1249b17e 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1091,9 +1091,13 @@ def is_vehicle_moving_supported(self) -> bool: @property def parking_time(self) -> str: """Return timestamp of last parking time.""" - park_time_utc: datetime = self.attrs.get("findCarResponse", {}).get("parkingTimeUTC", "Unknown") - park_time = park_time_utc.replace(tzinfo=timezone.utc).astimezone(tz=None) - return park_time.strftime("%Y-%m-%d %H:%M:%S") + park_time = "Unknown" + parking_time_path = "parkingposition.carCapturedTimestamp" + if is_valid_path(self.attrs, parking_time_path): + park_time_utc = find_path(self.attrs, parking_time_path) + park_time = park_time_utc.replace(tzinfo=timezone.utc).astimezone(tz=None) + return park_time.strftime("%Y-%m-%d %H:%M:%S") + return park_time @property def parking_time_last_updated(self) -> datetime: From 96b0c8ddc6aec23ffa9f4ef088417f3d8f9ece83 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 19:57:34 +0100 Subject: [PATCH 24/41] Introduction of API Status sensors --- volkswagencarnet/vw_dashboard.py | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/volkswagencarnet/vw_dashboard.py b/volkswagencarnet/vw_dashboard.py index f7fb88f4..d1f92daf 100644 --- a/volkswagencarnet/vw_dashboard.py +++ b/volkswagencarnet/vw_dashboard.py @@ -994,6 +994,42 @@ def create_instruments(): icon="mdi:chat-alert", unit="", ), + Sensor( + attr="api_vehicles_status", + name="API vehicles", + icon="mdi:api", + unit="", + ), + Sensor( + attr="api_capabilities_status", + name="API capabilities", + icon="mdi:api", + unit="", + ), + Sensor( + attr="api_trips_status", + name="API trips", + icon="mdi:api", + unit="", + ), + Sensor( + attr="api_selectivestatus_status", + name="API selectivestatus", + icon="mdi:api", + unit="", + ), + Sensor( + attr="api_parkingposition_status", + name="API parkingposition", + icon="mdi:api", + unit="", + ), + Sensor( + attr="api_token_status", + name="API token", + icon="mdi:api", + unit="", + ), BinarySensor(attr="external_power", name="External power", device_class=VWDeviceClass.POWER), BinarySensor(attr="energy_flow", name="Energy flow", device_class=VWDeviceClass.POWER), BinarySensor( From f4a55381da6147141840454354640936d3411202 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:02:53 +0100 Subject: [PATCH 25/41] Introduction of API Status sensors --- volkswagencarnet/vw_connection.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index ba24f1e2..d9bf7c16 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -81,6 +81,8 @@ def __init__(self, session, username, password, fulldebug=False, country=COUNTRY self._jarCookie = "" self._state = {} + self._service_status = {'vehicles': 'Unknown', 'capabilities': 'Unknown', 'selectivestatus': 'Unknown', 'trips': 'Unknown', 'parkingposition': 'Unknown', 'token': 'Unknown'} + def _clear_cookies(self): self._session._cookie_jar._cookies.clear() @@ -400,6 +402,9 @@ async def _request(self, method, url, **kwargs): else: self._jarCookie = response.cookies + # Update service status + await self.update_service_status(url, response.status) + try: if response.status == 204: res = {"status_code": response.status} @@ -1211,6 +1216,7 @@ async def refresh_tokens(self): response = await self._session.post( url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body ) + self.update_service_status("token", response.status) if response.status == 200: tokens = await response.json() # Verify Token @@ -1227,6 +1233,34 @@ async def refresh_tokens(self): _LOGGER.warning(f"Could not refresh tokens: {error}") return False + async def update_service_status(self, url, response_code): + """Update service status.""" + if response_code == 200 or response_code == 204: + status = "Up" + elif response_code == 207: + status = "Warning" + else: + status = "Down" + + if "vehicle/v2/vehicles" in url: + self._service_status["vehicles"] = status + elif "parkingposition" in url: + self._service_status["parkingposition"] = status + elif "/vehicle/v1/trips/" in url: + self._service_status["trips"] = status + elif "capabilities" in url: + self._service_status["capabilities"] = status + elif "selectivestatus" in url: + self._service_status["selectivestatus"] = status + elif "token" in url: + self._service_status["token"] = status + else: + _LOGGER.debug(f'Use-case is not covered! URL: {url}') + + async def get_service_status(self): + """Return list of service statuses.""" + return self._service_status + # Class helpers # @property def vehicles(self): From cb095471d17003c53a38795456d2249ed3653e29 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:06:47 +0100 Subject: [PATCH 26/41] Introduction of API Status sensors --- volkswagencarnet/vw_vehicle.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 1249b17e..e1a75d7a 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -185,6 +185,10 @@ async def update(self): # self.get_timerprogramming(), # return_exceptions=True, ) + await asyncio.sleep(10) + await asyncio.gather( + self.get_service_status() + ) else: _LOGGER.info(f"Vehicle with VIN {self.vin} is deactivated.") @@ -215,6 +219,12 @@ async def get_trip_last(self): if data: self._states.update(data) + async def get_service_status(self): + """Fetch service status.""" + data = await self._connection.get_service_status() + if data: + self._states.update({"service_status": data}) + async def get_realcardata(self): """Fetch realcardata.""" data = await self._connection.getRealCarData(self.vin) @@ -2566,3 +2576,93 @@ def is_secondary_drive_combustion(self): def has_combustion_engine(self): """Return true if car has a combustion engine.""" return self.is_primary_drive_combustion() or self.is_secondary_drive_combustion() + + @property + def api_vehicles_status(self) -> bool: + """Check vehicles API status.""" + return find_path(self.attrs, "service_status.vehicles") + + @property + def api_vehicles_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_vehicles_status_supported(self): + """Vehicles API status is always supported.""" + return True + + @property + def api_capabilities_status(self) -> bool: + """Check capabilities API status.""" + return find_path(self.attrs, "service_status.capabilities") + + @property + def api_capabilities_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_capabilities_status_supported(self): + """Capabilities API status is always supported.""" + return True + + @property + def api_trips_status(self) -> bool: + """Check trips API status.""" + return find_path(self.attrs, "service_status.trips") + + @property + def api_trips_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_trips_status_supported(self): + """Trips API status is always supported.""" + return True + + @property + def api_selectivestatus_status(self) -> bool: + """Check selectivestatus API status.""" + return find_path(self.attrs, "service_status.selectivestatus") + + @property + def api_selectivestatus_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_selectivestatus_status_supported(self): + """Selectivestatus API status is always supported.""" + return True + + @property + def api_parkingposition_status(self) -> bool: + """Check parkingposition API status.""" + return find_path(self.attrs, "service_status.parkingposition") + + @property + def api_parkingposition_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_parkingposition_status_supported(self): + """Parkingposition API status is always supported.""" + return True + + @property + def api_token_status(self) -> bool: + """Check token API status.""" + return find_path(self.attrs, "service_status.token") + + @property + def api_token_status_last_updated(self) -> datetime: + """Return attribute last updated timestamp.""" + return datetime.now() + + @property + def is_api_token_status_supported(self): + """Parkingposition API status is always supported.""" + return True From 2f862d9f333d047c091cc14290d0603df97d84f7 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:13:54 +0100 Subject: [PATCH 27/41] Add rate limit status --- volkswagencarnet/vw_connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index d9bf7c16..e389a682 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1239,6 +1239,8 @@ async def update_service_status(self, url, response_code): status = "Up" elif response_code == 207: status = "Warning" + elif response_code == 429: + status = "Rate limited" else: status = "Down" From b4317320943b5edd82aaa155b17edf397097d563 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 21:10:30 +0100 Subject: [PATCH 28/41] Update vw_connection.py --- volkswagencarnet/vw_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index e389a682..9d4559d3 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -81,7 +81,7 @@ def __init__(self, session, username, password, fulldebug=False, country=COUNTRY self._jarCookie = "" self._state = {} - self._service_status = {'vehicles': 'Unknown', 'capabilities': 'Unknown', 'selectivestatus': 'Unknown', 'trips': 'Unknown', 'parkingposition': 'Unknown', 'token': 'Unknown'} + self._service_status = {} def _clear_cookies(self): self._session._cookie_jar._cookies.clear() From 9beb7f7f5e57eeea28aeaa1ff6fd8a369749e84a Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 21:11:47 +0100 Subject: [PATCH 29/41] Small fix --- volkswagencarnet/vw_vehicle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index e1a75d7a..1b65f9fe 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -2580,7 +2580,7 @@ def has_combustion_engine(self): @property def api_vehicles_status(self) -> bool: """Check vehicles API status.""" - return find_path(self.attrs, "service_status.vehicles") + return self.attrs.get("service_status", {}).get("vehicles", "Unknown") @property def api_vehicles_status_last_updated(self) -> datetime: @@ -2595,7 +2595,7 @@ def is_api_vehicles_status_supported(self): @property def api_capabilities_status(self) -> bool: """Check capabilities API status.""" - return find_path(self.attrs, "service_status.capabilities") + return self.attrs.get("service_status", {}).get("capabilities", "Unknown") @property def api_capabilities_status_last_updated(self) -> datetime: @@ -2610,7 +2610,7 @@ def is_api_capabilities_status_supported(self): @property def api_trips_status(self) -> bool: """Check trips API status.""" - return find_path(self.attrs, "service_status.trips") + return self.attrs.get("service_status", {}).get("trips", "Unknown") @property def api_trips_status_last_updated(self) -> datetime: @@ -2625,7 +2625,7 @@ def is_api_trips_status_supported(self): @property def api_selectivestatus_status(self) -> bool: """Check selectivestatus API status.""" - return find_path(self.attrs, "service_status.selectivestatus") + return self.attrs.get("service_status", {}).get("selectivestatus", "Unknown") @property def api_selectivestatus_status_last_updated(self) -> datetime: @@ -2640,7 +2640,7 @@ def is_api_selectivestatus_status_supported(self): @property def api_parkingposition_status(self) -> bool: """Check parkingposition API status.""" - return find_path(self.attrs, "service_status.parkingposition") + return self.attrs.get("service_status", {}).get("parkingposition", "Unknown") @property def api_parkingposition_status_last_updated(self) -> datetime: @@ -2655,7 +2655,7 @@ def is_api_parkingposition_status_supported(self): @property def api_token_status(self) -> bool: """Check token API status.""" - return find_path(self.attrs, "service_status.token") + return self.attrs.get("service_status", {}).get("token", "Unknown") @property def api_token_status_last_updated(self) -> datetime: From fed122bc039400747999575d564024daee8f0707 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 21:58:53 +0100 Subject: [PATCH 30/41] Update response codes --- volkswagencarnet/vw_connection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 9d4559d3..9b17cbb4 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1235,10 +1235,8 @@ async def refresh_tokens(self): async def update_service_status(self, url, response_code): """Update service status.""" - if response_code == 200 or response_code == 204: + if response_code in [200, 204, 207]: status = "Up" - elif response_code == 207: - status = "Warning" elif response_code == 429: status = "Rate limited" else: From 26c5c222da861ed42c626e28ea2f10643ccc2bc4 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Fri, 8 Dec 2023 23:18:01 +0100 Subject: [PATCH 31/41] Small fix and more status codes --- volkswagencarnet/vw_connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 9b17cbb4..49fa497e 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1216,7 +1216,7 @@ async def refresh_tokens(self): response = await self._session.post( url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body ) - self.update_service_status("token", response.status) + await self.update_service_status("token", response.status) if response.status == 200: tokens = await response.json() # Verify Token @@ -1237,6 +1237,10 @@ async def update_service_status(self, url, response_code): """Update service status.""" if response_code in [200, 204, 207]: status = "Up" + elif response_code == 401: + status = "Unauthorized" + elif response_code == 403: + status = "Forbidden" elif response_code == 429: status = "Rate limited" else: From 81ee2b42c6b72b821d1f46247eec1589f538bd88 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Sat, 9 Dec 2023 18:41:52 +0100 Subject: [PATCH 32/41] remove unnecessary delay --- volkswagencarnet/vw_vehicle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 1b65f9fe..36f37469 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -185,7 +185,6 @@ async def update(self): # self.get_timerprogramming(), # return_exceptions=True, ) - await asyncio.sleep(10) await asyncio.gather( self.get_service_status() ) From b85806e70140fbbc4457f3ddaf9ee583fccb3796 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Sat, 9 Dec 2023 18:46:20 +0100 Subject: [PATCH 33/41] more robust error detection --- volkswagencarnet/vw_connection.py | 83 +++++++++++++++++-------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 49fa497e..d92695b8 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -385,46 +385,55 @@ async def logout(self): async def _request(self, method, url, **kwargs): """Perform a query to the VW-Group API.""" _LOGGER.debug(f'HTTP {method} "{url}"') - async with self._session.request( - method, - url, - headers=self._session_headers, - timeout=ClientTimeout(total=TIMEOUT.seconds), - cookies=self._jarCookie, - raise_for_status=False, - **kwargs, - ) as response: - response.raise_for_status() - - # Update cookie jar - if self._jarCookie != "": - self._jarCookie.update(response.cookies) - else: - self._jarCookie = response.cookies + try: + async with self._session.request( + method, + url, + headers=self._session_headers, + timeout=ClientTimeout(total=TIMEOUT.seconds), + cookies=self._jarCookie, + raise_for_status=False, + **kwargs, + ) as response: + response.raise_for_status() + + # Update cookie jar + if self._jarCookie != "": + self._jarCookie.update(response.cookies) + else: + self._jarCookie = response.cookies - # Update service status - await self.update_service_status(url, response.status) + # Update service status + await self.update_service_status(url, response.status) - try: - if response.status == 204: - res = {"status_code": response.status} - elif response.status >= 200 or response.status <= 300: - res = await response.json(loads=json_loads) - else: + try: + if response.status == 204: + res = {"status_code": response.status} + elif response.status >= 200 or response.status <= 300: + res = await response.json(loads=json_loads) + else: + res = {} + _LOGGER.debug(f"Not success status code [{response.status}] response: {response.text}") + if "X-RateLimit-Remaining" in response.headers: + res["rate_limit_remaining"] = response.headers.get("X-RateLimit-Remaining", "") + except Exception: res = {} - _LOGGER.debug(f"Not success status code [{response.status}] response: {response.text}") - if "X-RateLimit-Remaining" in response.headers: - res["rate_limit_remaining"] = response.headers.get("X-RateLimit-Remaining", "") - except Exception: - res = {} - _LOGGER.debug(f"Something went wrong [{response.status}] response: {response.text}") - return res + _LOGGER.debug(f"Something went wrong [{response.status}] response: {response.text}") + return res - if self._session_fulldebug: - _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res}') - else: - _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]') - return res + if self._session_fulldebug: + _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}], response: {res}') + else: + _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]') + return res + except client_exceptions.ClientResponseError as httperror: + # Update service status + await self.update_service_status(url, httperror.code) + raise httperror from None + except Exception as error: + # Update service status + await self.update_service_status(url, 1000) + raise error from None async def get(self, url, vin="", tries=0): """Perform a get query.""" @@ -1243,6 +1252,8 @@ async def update_service_status(self, url, response_code): status = "Forbidden" elif response_code == 429: status = "Rate limited" + elif response_code == 1000: + status = "Error" else: status = "Down" From c94933550f3df0a77fc63df74dfcc4f217f3c3de Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:21:50 +0100 Subject: [PATCH 34/41] Add logging --- volkswagencarnet/vw_connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index d92695b8..972295a9 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1270,10 +1270,11 @@ async def update_service_status(self, url, response_code): elif "token" in url: self._service_status["token"] = status else: - _LOGGER.debug(f'Use-case is not covered! URL: {url}') + _LOGGER.debug(f'Unhandled API URL: "{url}"') async def get_service_status(self): """Return list of service statuses.""" + _LOGGER.debug("Getting API status updates") return self._service_status # Class helpers # From 9230c31657cab9357257d2846555e45207bae182 Mon Sep 17 00:00:00 2001 From: stickpin <630000+stickpin@users.noreply.github.com> Date: Sun, 10 Dec 2023 10:40:30 +0100 Subject: [PATCH 35/41] Fix refresh_tokens flow A new token is not being set for the Authorization header after the token refresh. --- volkswagencarnet/vw_connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index ba24f1e2..a0f64a46 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1218,6 +1218,7 @@ async def refresh_tokens(self): _LOGGER.warning("Token could not be verified!") for token in tokens: self._session_tokens["identity"][token] = tokens[token] + self._session_headers["Authorization"] = "Bearer " + self._session_tokens["identity"]["access_token"] else: _LOGGER.warning(f"Something went wrong when refreshing {BRAND} account tokens.") return False From 4d96605fd412f7a4c46c251fdb72228af228942d Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:47:35 +0100 Subject: [PATCH 36/41] Fix isMoving sensor --- volkswagencarnet/vw_connection.py | 12 +++++++++--- volkswagencarnet/vw_vehicle.py | 13 ++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 82069a25..8f5a094f 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -605,10 +605,16 @@ async def getParkingPosition(self, vin): response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", "") if "data" in response: - return {"parkingposition": response["data"]} + return {"isMoving": False, "parkingposition": response["data"]} + elif response.get("status_code", {}): + if response.get("status_code", 0) == 204: + _LOGGER.debug("Seems car is moving, HTTP 204 received from parkingposition") + data = {"isMoving": True, "parkingposition": {}} + return data + else: + _LOGGER.warning(f'Could not fetch parkingposition, HTTP status code: {response.get("status_code")}') else: - return {"parkingposition": {}} - + _LOGGER.info("Unhandled error while trying to fetch parkingposition data") except Exception as error: _LOGGER.warning(f"Could not fetch parkingposition, error: {error}") return False diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 36f37469..1cb4b6b8 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1074,23 +1074,22 @@ def position(self) -> dict[str, str | float | None]: @property def position_last_updated(self) -> datetime: """Return position last updated.""" - return find_path(self.attrs, "parkingposition.carCapturedTimestamp") + return self.attrs.get("parkingposition", {}).get("carCapturedTimestamp", "Unknown") @property def is_position_supported(self) -> bool: """Return true if position is available.""" - return is_valid_path(self.attrs, "parkingposition.carCapturedTimestamp") + return is_valid_path(self.attrs, "parkingposition.carCapturedTimestamp") or self.attrs.get("isMoving", False) @property def vehicle_moving(self) -> bool: """Return true if vehicle is moving.""" - # there is not "isMoving" property anymore in VW's API, so we just take the absence of position data as the indicator - return not is_valid_path(self.attrs, "parkingposition.lat") + return self.attrs.get("isMoving", False) @property def vehicle_moving_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return find_path(self.attrs, "parkingposition.carCapturedTimestamp") + return self.position_last_updated @property def is_vehicle_moving_supported(self) -> bool: @@ -1111,12 +1110,12 @@ def parking_time(self) -> str: @property def parking_time_last_updated(self) -> datetime: """Return attribute last updated timestamp.""" - return self.attrs.get("findCarResponse", {}).get("Position", {}).get(BACKEND_RECEIVED_TIMESTAMP) + return self.position_last_updated @property def is_parking_time_supported(self) -> bool: """Return true if vehicle parking timestamp is supported.""" - return "parkingTimeUTC" in self.attrs.get("findCarResponse", {}) + return self.is_position_supported # Vehicle fuel level and range @property From 36117267dfe9d5d345484ad8f0eab366133c4900 Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:51:24 +0100 Subject: [PATCH 37/41] Fix for Doors and Trunk sensors --- volkswagencarnet/vw_vehicle.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 36f37469..a2190012 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1717,7 +1717,12 @@ def is_door_locked_supported(self) -> bool: :return: """ - return is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus") + # First check that the service is actually enabled + if not self._services.get("access", {}).get("active", False): + return False + if is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus"): + return True + return False @property def is_door_locked_sensor_supported(self) -> bool: @@ -1726,7 +1731,12 @@ def is_door_locked_sensor_supported(self) -> bool: :return: """ - return is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus") + # Use real lock if the service is actually enabled + if self._services.get("access", {}).get("active", False): + return False + if is_valid_path(self.attrs, "access.accessStatus.value.doorLockStatus"): + return True + return False @property def trunk_locked(self) -> bool: @@ -1753,6 +1763,8 @@ def is_trunk_locked_supported(self) -> bool: :return: """ + if not self._services.get("access", {}).get("active", False): + return False if is_valid_path(self.attrs, "access.accessStatus.value.doors"): doors = find_path(self.attrs, "access.accessStatus.value.doors") for door in doors: @@ -1785,6 +1797,8 @@ def is_trunk_locked_sensor_supported(self) -> bool: :return: """ + if self._services.get("access", {}).get("active", False): + return False if is_valid_path(self.attrs, "access.accessStatus.value.doors"): doors = find_path(self.attrs, "access.accessStatus.value.doors") for door in doors: From d662539323602a8bc3c39813462bd50650e4e9f2 Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:06:51 +0100 Subject: [PATCH 38/41] Fix combined range sensor logic --- volkswagencarnet/vw_vehicle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 36f37469..7ff8a0d2 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1194,8 +1194,9 @@ def is_combined_range_supported(self) -> bool: :return: """ - return is_valid_path(self.attrs, "measurements.rangeStatus.value.totalRange_km") - + if is_valid_path(self.attrs, "measurements.rangeStatus.value.totalRange_km"): + return self.is_electric_range_supported and self.is_combustion_range_supported + return False @property def fuel_level(self) -> int: """ From b9c0076719f0c7e15d78bc8e30d59f0bebf09dc1 Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:18:07 +0100 Subject: [PATCH 39/41] code fixes for lint with black --- volkswagencarnet/vw_connection.py | 2 +- volkswagencarnet/vw_const.py | 2 +- volkswagencarnet/vw_vehicle.py | 70 +++++++++++++------------------ 3 files changed, 30 insertions(+), 44 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 82069a25..d0ef0957 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1220,7 +1220,7 @@ async def refresh_tokens(self): body = { "grant_type": "refresh_token", "refresh_token": self._session_tokens["identity"]["refresh_token"], - "client_id": CLIENT["Legacy"]["CLIENT_ID"] + "client_id": CLIENT["Legacy"]["CLIENT_ID"], } response = await self._session.post( url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body diff --git a/volkswagencarnet/vw_const.py b/volkswagencarnet/vw_const.py index 861bc962..4ddaff82 100644 --- a/volkswagencarnet/vw_const.py +++ b/volkswagencarnet/vw_const.py @@ -11,7 +11,7 @@ "Legacy": { "CLIENT_ID": "a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com", "SCOPE": "openid profile badge cars dealers vin", - "TOKEN_TYPES": "code" + "TOKEN_TYPES": "code", } } diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 36f37469..3a5828f3 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -68,7 +68,7 @@ def __init__(self, conn, url): "tripStatistics": {"active": False}, "measurements": {"active": False}, "honkAndFlash": {"active": False}, - "parkingPosition": {"active" : False} + "parkingPosition": {"active": False} # "rheating_v1": {"active": False}, # "rclima_v1": {"active": False}, # "statusreport_v1": {"active": False}, @@ -185,9 +185,7 @@ async def update(self): # self.get_timerprogramming(), # return_exceptions=True, ) - await asyncio.gather( - self.get_service_status() - ) + await asyncio.gather(self.get_service_status()) else: _LOGGER.info(f"Vehicle with VIN {self.vin} is deactivated.") @@ -1229,8 +1227,9 @@ def is_fuel_level_supported(self) -> bool: :return: """ - return (is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") - or is_valid_path(self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct")) + return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") or is_valid_path( + self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct") + ) # Climatisation settings @property @@ -2139,10 +2138,9 @@ def is_trip_last_average_speed_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.averageSpeed_kmph") and - type(find_path(self.attrs, "trip_last.averageSpeed_kmph")) in (float, int) - ) + return is_valid_path(self.attrs, "trip_last.averageSpeed_kmph") and type( + find_path(self.attrs, "trip_last.averageSpeed_kmph") + ) in (float, int) @property def trip_last_average_electric_engine_consumption(self): @@ -2165,10 +2163,9 @@ def is_trip_last_average_electric_engine_consumption_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.averageElectricConsumption") and - type(find_path(self.attrs, "trip_last.averageElectricConsumption")) in (float, int) - ) + return is_valid_path(self.attrs, "trip_last.averageElectricConsumption") and type( + find_path(self.attrs, "trip_last.averageElectricConsumption") + ) in (float, int) @property def trip_last_average_fuel_consumption(self): @@ -2191,10 +2188,9 @@ def is_trip_last_average_fuel_consumption_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.averageFuelConsumption") and - type(find_path(self.attrs, "trip_last.averageFuelConsumption")) in (float, int) - ) + return is_valid_path(self.attrs, "trip_last.averageFuelConsumption") and type( + find_path(self.attrs, "trip_last.averageFuelConsumption") + ) in (float, int) @property def trip_last_average_auxillary_consumption(self): @@ -2211,7 +2207,6 @@ def trip_last_average_auxillary_consumption_last_updated(self) -> datetime: """Return last updated timestamp.""" return find_path(self.attrs, "trip_last.tripEndTimestamp") - @property def is_trip_last_average_auxillary_consumption_supported(self) -> bool: """ @@ -2219,11 +2214,9 @@ def is_trip_last_average_auxillary_consumption_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.averageAuxiliaryConsumption") and - type(find_path(self.attrs, "trip_last.averageAuxiliaryConsumption")) in (float, int) - ) - + return is_valid_path(self.attrs, "trip_last.averageAuxiliaryConsumption") and type( + find_path(self.attrs, "trip_last.averageAuxiliaryConsumption") + ) in (float, int) @property def trip_last_average_aux_consumer_consumption(self): @@ -2247,10 +2240,9 @@ def is_trip_last_average_aux_consumer_consumption_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.averageAuxConsumerConsumption") and - type(find_path(self.attrs, "trip_last.averageAuxConsumerConsumption")) in (float, int) - ) + return is_valid_path(self.attrs, "trip_last.averageAuxConsumerConsumption") and type( + find_path(self.attrs, "trip_last.averageAuxConsumerConsumption") + ) in (float, int) @property def trip_last_duration(self): @@ -2261,7 +2253,6 @@ def trip_last_duration(self): """ return find_path(self.attrs, "trip_last.travelTime") - @property def trip_last_duration_last_updated(self) -> datetime: """Return last updated timestamp.""" @@ -2274,11 +2265,9 @@ def is_trip_last_duration_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.travelTime") and - type(find_path(self.attrs, "trip_last.travelTime")) in (float, int) - ) - + return is_valid_path(self.attrs, "trip_last.travelTime") and type( + find_path(self.attrs, "trip_last.travelTime") + ) in (float, int) @property def trip_last_length(self): @@ -2294,7 +2283,6 @@ def trip_last_length_last_updated(self) -> datetime: """Return last updated timestamp.""" return find_path(self.attrs, "trip_last.tripEndTimestamp") - @property def is_trip_last_length_supported(self) -> bool: """ @@ -2302,11 +2290,9 @@ def is_trip_last_length_supported(self) -> bool: :return: """ - return ( - is_valid_path(self.attrs, "trip_last.mileage_km") and - type(find_path(self.attrs, "trip_last.mileage_km")) in (float, int) - ) - + return is_valid_path(self.attrs, "trip_last.mileage_km") and type( + find_path(self.attrs, "trip_last.mileage_km") + ) in (float, int) @property def trip_last_recuperation(self): @@ -2546,8 +2532,8 @@ def is_primary_drive_electric(self): def is_secondary_drive_electric(self): """Check if secondary engine is electric.""" return ( - is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") and - find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") == ENGINE_TYPE_ELECTRIC + is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") + and find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") == ENGINE_TYPE_ELECTRIC ) def is_primary_drive_combustion(self): From 053aec43340d2fe4392aca745eef8a654b82a7fc Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:31:20 +0100 Subject: [PATCH 40/41] black fix --- volkswagencarnet/vw_vehicle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index bea8beb9..2022b6f9 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1228,7 +1228,7 @@ def is_fuel_level_supported(self) -> bool: :return: """ return is_valid_path(self.attrs, "measurements.fuelLevelStatus.value.currentFuelLevel_pct") or is_valid_path( - self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct") + self.attrs, "fuelStatus.rangeStatus.value.primaryEngine.currentFuelLevel_pct" ) # Climatisation settings From c9b702de18cb84d0119a1fadc173bfa3d3f96d47 Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:33:35 +0100 Subject: [PATCH 41/41] black fix --- volkswagencarnet/vw_vehicle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 2022b6f9..0ec0a46d 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1194,6 +1194,7 @@ def is_combined_range_supported(self) -> bool: if is_valid_path(self.attrs, "measurements.rangeStatus.value.totalRange_km"): return self.is_electric_range_supported and self.is_combustion_range_supported return False + @property def fuel_level(self) -> int: """ @@ -2542,7 +2543,6 @@ def is_primary_drive_electric(self): """Check if primary engine is electric.""" return find_path(self.attrs, "measurements.fuelLevelStatus.value.primaryEngineType") == ENGINE_TYPE_ELECTRIC - def is_secondary_drive_electric(self): """Check if secondary engine is electric.""" return (