From 77b061561f25f37243a94adc233870151e75af36 Mon Sep 17 00:00:00 2001 From: t0bias-r Date: Fri, 1 Mar 2024 08:11:50 +0100 Subject: [PATCH] partial update to new Audi API tested with Q8 e-tron known limitations: setting climatisation and other setters fail --- custom_components/audiconnect/audi_api.py | 6 +- .../audiconnect/audi_connect_account.py | 47 ++--- custom_components/audiconnect/audi_models.py | 165 ++++++++++++++++-- .../audiconnect/audi_services.py | 23 ++- .../audiconnect/device_tracker.py | 4 +- custom_components/audiconnect/manifest.json | 1 - custom_components/audiconnect/util.py | 4 +- hacs.json | 1 + 8 files changed, 185 insertions(+), 66 deletions(-) diff --git a/custom_components/audiconnect/audi_api.py b/custom_components/audiconnect/audi_api.py index cbe1bc6b..a15b5285 100644 --- a/custom_components/audiconnect/audi_api.py +++ b/custom_components/audiconnect/audi_api.py @@ -19,8 +19,8 @@ class AudiAPI: - HDR_XAPP_VERSION = "4.16.0" - HDR_USER_AGENT = "myAudi-Android/4.13.0 (Build 800238275.2210271555) Android/11" + HDR_XAPP_VERSION = "4.23.1" + HDR_USER_AGENT = "Android/4.23.1 (Build 800240120.root project 'onetouch-android'.ext.buildTime) Android/11" def __init__(self, session, proxy=None): self.__token = None @@ -60,7 +60,7 @@ async def request( return response, txt elif raw_contents: return await response.read() - elif response.status == 200 or response.status == 202: + elif response.status == 200 or response.status == 202 or response.status == 207: return await response.json(loads=json_loads) else: raise ClientResponseError( diff --git a/custom_components/audiconnect/audi_connect_account.py b/custom_components/audiconnect/audi_connect_account.py index b3bbe409..948ac0ad 100644 --- a/custom_components/audiconnect/audi_connect_account.py +++ b/custom_components/audiconnect/audi_connect_account.py @@ -407,12 +407,12 @@ async def update(self): await self.call_update(self.update_vehicle_longterm, 3) info = "position" await self.call_update(self.update_vehicle_position, 3) - info = "climater" - await self.call_update(self.update_vehicle_climater, 3) - info = "charger" - await self.call_update(self.update_vehicle_charger, 3) - info = "preheater" - await self.call_update(self.update_vehicle_preheater, 3) + #info = "climater" + #await self.call_update(self.update_vehicle_climater, 3) + #info = "charger" + #await self.call_update(self.update_vehicle_charger, 3) + #info = "preheater" + #await self.call_update(self.update_vehicle_preheater, 3) # Return True on success, False on error return self._no_error except Exception as exception: @@ -426,7 +426,7 @@ def log_exception_once(self, exception, message): err = message + ": " + str(exception).rstrip("\n") if not err in self._logged_errors: self._logged_errors.add(err) - _LOGGER.error(err) + _LOGGER.error(err, exc_info=True) async def update_vehicle_statusreport(self): if not self.support_status_report: @@ -439,6 +439,8 @@ async def update_vehicle_statusreport(self): for i in range(0, len(status.data_fields)) } self._vehicle.state["last_update_time"] = status.data_fields[0].send_time + for state in status.states: + self._vehicle.state[state["name"]] = state["value"] except TimeoutError: raise @@ -471,22 +473,12 @@ async def update_vehicle_position(self): try: resp = await self._audi_service.get_stored_position(self._vehicle.vin) - if resp.get("findCarResponse") is not None: - position = resp["findCarResponse"] - - if ( - position.get("Position") is not None - and position["Position"].get("carCoordinate") is not None - ): + if resp is not None: self._vehicle.state["position"] = { - "latitude": get_attr(position, "Position.carCoordinate.latitude") - / 1000000, - "longitude": get_attr(position, "Position.carCoordinate.longitude") - / 1000000, - "timestamp": get_attr(position, "Position.timestampCarSentUTC"), - "parktime": position.get("parkingTimeUTC") - if position.get("parkingTimeUTC") is not None - else get_attr(position, "Position.timestampCarSentUTC"), + "latitude": resp["data"]["lat"], + "longitude": resp["data"]["lon"], + "timestamp": resp["data"]["carCapturedTimestamp"], + "parktime": resp["data"]["carCapturedTimestamp"] } except TimeoutError: @@ -613,7 +605,7 @@ async def update_vehicle_charger(self): result, "charger.status.chargingStatusData.actualChargeRate.content" ) if self._vehicle.state["actualChargeRate"] is not None: - self._vehicle.state["actualChargeRate"] = float(self._vehicle.state["actualChargeRate"]) / 10 + self._vehicle.state["actualChargeRate"] = float(self._vehicle.state["actualChargeRate"]) self._vehicle.state["actualChargeRateUnit"] = get_attr( result, "charger.status.chargingStatusData.chargeRateUnit.content" ) @@ -1203,19 +1195,14 @@ def actual_charge_rate_supported(self): @property def actual_charge_rate_unit(self): - if self.actual_charge_rate_supported: - res = self._vehicle.state.get("actualChargeRateUnit") - if res: - return res.replace("_per_", "/") - - return res + return "km/h" @property def charging_power(self): """Return charging power""" if self.charging_power_supported: try: - return parse_int(self._vehicle.state.get("chargingPower")) / 1000 + return parse_float(self._vehicle.state.get("chargingPower")) except ValueError: return -1 diff --git a/custom_components/audiconnect/audi_models.py b/custom_components/audiconnect/audi_models.py index ac33a1f1..1b19a545 100644 --- a/custom_components/audiconnect/audi_models.py +++ b/custom_components/audiconnect/audi_models.py @@ -17,23 +17,154 @@ def __init__(self, data): class VehicleDataResponse: + OLDAPI_MAPPING = { + "frontRightLock": "LOCK_STATE_RIGHT_FRONT_DOOR", + "frontRightOpen": "OPEN_STATE_RIGHT_FRONT_DOOR", + "frontLeftLock": "LOCK_STATE_LEFT_FRONT_DOOR", + "frontLeftOpen": "OPEN_STATE_LEFT_FRONT_DOOR", + "rearRightLock": "LOCK_STATE_RIGHT_REAR_DOOR", + "rearRightOpen": "OPEN_STATE_RIGHT_REAR_DOOR", + "rearLeftLock": "LOCK_STATE_LEFT_REAR_DOOR", + "rearLeftOpen": "OPEN_STATE_LEFT_REAR_DOOR", + "trunkLock": "LOCK_STATE_TRUNK_LID", + "trunkOpen": "OPEN_STATE_TRUNK_LID", + "frontLeftWindow" : "STATE_LEFT_FRONT_WINDOW", + "frontRightWindow" : "STATE_RIGHT_FRONT_WINDOW", + "rearLeftWindow" : "STATE_LEFT_REAR_WINDOW", + "rearRightWindow" : "STATE_RIGHT_REAR_WINDOW", + "sunRoof": "STATE_SUN_ROOF_MOTOR_COVER", + "bonnet": "OPEN_STATE_HOOD" + } + def __init__(self, data): self.data_fields = [] - response = data.get("StoredVehicleDataResponse") - if response is None: - response = data.get("CurrentVehicleDataByRequestResponse") - - vehicle_data = response.get("vehicleData") - if vehicle_data is None: - return - - vehicle_data = vehicle_data.get("data") - for raw_data in vehicle_data: - raw_fields = raw_data.get("field") - if raw_fields is None: + self.states = [] + + self.data_fields.append(Field({ + "textId": "TOTAL_RANGE", + "value": data["charging"]["batteryStatus"]["value"]["cruisingRangeElectric_km"], + "tsCarCaptured": data["charging"]["batteryStatus"]["value"]["carCapturedTimestamp"], + })) + + self.data_fields.append(Field({ + "textId": "UTC_TIME_AND_KILOMETER_STATUS", + "value": data["measurements"]["odometerStatus"]["value"]["odometer"], + "tsCarCaptured": data["measurements"]["odometerStatus"]["value"]["carCapturedTimestamp"], + })) + + self.data_fields.append(Field({ + "textId": "MAINTENANCE_INTERVAL_TIME_TO_INSPECTION", + "value": data["vehicleHealthInspection"]["maintenanceStatus"]["value"]["inspectionDue_days"], + "tsCarCaptured": data["vehicleHealthInspection"]["maintenanceStatus"]["value"]["carCapturedTimestamp"], + })) + + self.data_fields.append(Field({ + "textId": "MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION", + "value": data["vehicleHealthInspection"]["maintenanceStatus"]["value"]["inspectionDue_km"], + "tsCarCaptured": data["vehicleHealthInspection"]["maintenanceStatus"]["value"]["carCapturedTimestamp"], + })) + + self.appendWindowState(data) + self.appendSunRoofState(data) + self.appendDoorState(data) + self.appendHoodState(data) + + self.states.append({"name" : "stateOfCharge", "value" : data["measurements"]["fuelLevelStatus"]["value"]["currentSOC_pct"], "measure_time" : data["measurements"]["fuelLevelStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "chargingMode", "value" : data["charging"]["chargingStatus"]["value"]["chargeType"], "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "actualChargeRate", "value" : data["charging"]["chargingStatus"]["value"]["chargeRate_kmph"], "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "chargingPower", "value" : data["charging"]["chargingStatus"]["value"]["chargePower_kW"], "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "chargeMode", "value" : data["charging"]["chargingStatus"]["value"]["chargeMode"], "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "chargingState", "value" : data["charging"]["chargingStatus"]["value"]["chargingState"], "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + self.states.append({"name" : "plugState", "value" : data["charging"]["plugStatus"] ["value"]["plugConnectionState"], "measure_time" : data["charging"]["plugStatus"] ["value"]["carCapturedTimestamp"] }) + #self.states.append({"name" : "remainingChargingTime", "value" : data["charging"]["chargingStatus"]["value"]["remainingChargingTimeToComplete_min"] "measure_time" : data["charging"]["chargingStatus"]["value"]["carCapturedTimestamp"] }) + + def appendHoodState(self, data): + doors = data["access"]["accessStatus"]["value"]["doors"]; + tsCarCapturedAccess = data["access"]["accessStatus"]["value"]["carCapturedTimestamp"]; + for door in doors: + status = door["status"] + name = door["name"] + if not name in self.OLDAPI_MAPPING: + continue + status = door["status"] + open = "0" + unsupported = False + for state in status: + if state == "unsupported": + unsupported = True + if state == "closed": + open = "3" + if (not unsupported): + doorFieldOpen = { + "textId": self.OLDAPI_MAPPING[name], + "value": open, + "tsCarCaptured": tsCarCapturedAccess, + } + self.data_fields.append(Field(doorFieldOpen)) + + def appendDoorState(self, data): + doors = data["access"]["accessStatus"]["value"]["doors"]; + tsCarCapturedAccess = data["access"]["accessStatus"]["value"]["carCapturedTimestamp"]; + for door in doors: + status = door["status"] + name = door["name"] + if not name+"Lock" in self.OLDAPI_MAPPING: + continue + status = door["status"] + lock = "0" + open = "0" + unsupported = False + for state in status: + if state == "unsupported": + unsupported = True + if state == "locked": + lock = "2" + if state == "closed": + open = "3" + if (not unsupported): + doorFieldLock = { + "textId": self.OLDAPI_MAPPING[name+"Lock"], + "value": lock, + "tsCarCaptured": tsCarCapturedAccess, + } + self.data_fields.append(Field(doorFieldLock)) + + doorFieldOpen = { + "textId": self.OLDAPI_MAPPING[name+"Open"], + "value": open, + "tsCarCaptured": tsCarCapturedAccess, + } + self.data_fields.append(Field(doorFieldOpen)) + + def appendSunRoofState(self, data): + windows = data["access"]["accessStatus"]["value"]["windows"]; + tsCarCapturedAccess = data["access"]["accessStatus"]["value"]["carCapturedTimestamp"]; + for window in windows: + name = window["name"] + status = window["status"] + if (status[0] == "unsupported") or not name in self.OLDAPI_MAPPING: + continue + windowField = { + "textId": self.OLDAPI_MAPPING[name], + "value": "3" if status[0] == "closed" else "0", + "tsCarCaptured": tsCarCapturedAccess, + } + self.data_fields.append(Field(windowField)) + + def appendWindowState(self, data): + windows = data["access"]["accessStatus"]["value"]["windows"]; + tsCarCapturedAccess = data["access"]["accessStatus"]["value"]["carCapturedTimestamp"]; + for window in windows: + name = window["name"] + status = window["status"] + if (status[0] == "unsupported") or not name+"Window" in self.OLDAPI_MAPPING: continue - for raw_field in raw_fields: - self.data_fields.append(Field(raw_field)) + windowField = { + "textId": self.OLDAPI_MAPPING[name + "Window"], + "value": "3" if status[0] == "closed" else "0", + "tsCarCaptured": tsCarCapturedAccess, + } + self.data_fields.append(Field(windowField)) class TripDataResponse: @@ -132,8 +263,10 @@ def __init__(self, data): self.id = data.get("id") self.unit = data.get("unit") self.value = data.get("value") - self.measure_time = data.get("tsCarCaptured") - self.send_time = data.get("tsCarSent") + self.measure_time = data.get("tsTssReceivedUtc") + if self.measure_time is None: + self.measure_time = data.get("tsCarCaptured") + self.send_time = data.get("tsCarSentUtc") self.measure_mileage = data.get("milCarCaptured") self.send_mileage = data.get("milCarSent") diff --git a/custom_components/audiconnect/audi_services.py b/custom_components/audiconnect/audi_services.py index bfb3d1fd..5a5e0f47 100644 --- a/custom_components/audiconnect/audi_services.py +++ b/custom_components/audiconnect/audi_services.py @@ -171,11 +171,10 @@ async def get_preheater(self, vin: str): ) async def get_stored_vehicle_data(self, vin: str): - self._api.use_token(self.vwToken) + self._api.use_token(self._bearer_token_json) data = await self._api.get( - "{homeRegion}/fs-car/bs/vsr/v1/{type}/{country}/vehicles/{vin}/status".format( - homeRegion=await self._get_home_region(vin.upper()), - type=self._type, country=self._country, vin=vin.upper() + "https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/selectivestatus?jobs=charging,chargingTimers,chargingProfiles,fuelStatus,measurements,oilLevel,vehicleHealthInspection,access,vehicleLights,vehicleHealthWarnings".format( + vin=vin.upper(), ) ) return VehicleDataResponse(data) @@ -199,11 +198,10 @@ async def get_climater(self, vin: str): ) async def get_stored_position(self, vin: str): - self._api.use_token(self.vwToken) + self._api.use_token(self._bearer_token_json); return await self._api.get( - "{homeRegion}/fs-car/bs/cf/v1/{type}/{country}/vehicles/{vin}/position".format( - homeRegion=await self._get_home_region(vin.upper()), - type=self._type, country=self._country, vin=vin.upper() + "https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/parkingposition".format( + vin=vin.upper(), ) ) @@ -250,7 +248,8 @@ async def get_vehicle_information(self): } req_rsp, rep_rsptxt = await self._api.request( "POST", - "https://app-api.my.aoa.audi.com/vgql/v1/graphql" if self._country.upper()=="US" else "https://app-api.live-my.audi.com/vgql/v1/graphql", # Starting in 2023, US users need to point at the aoa (Audi of America) URL. + #"https://app-api.my.aoa.audi.com/vgql/v1/graphql" if self._country.upper()=="US" else "https://app-api.live-my.audi.com/vgql/v1/graphql", # Starting in 2023, US users need to point at the aoa (Audi of America) URL. + "https://app-api.live-my.audi.com/vgql/v1/graphql", json.dumps(req_data), headers=headers, allow_redirects=False, @@ -755,7 +754,7 @@ async def login_request(self, user: str, password: str): ]["defaultLanguage"] # Dynamic configuration URLs - marketcfg_url = "https://content.app.my.audi.com/service/mobileapp/configurations/market/{c}/{l}?v=4.15.0".format( + marketcfg_url = "https://content.app.my.audi.com/service/mobileapp/configurations/market/{c}/{l}?v=4.23.1".format( c=self._country, l=self._language ) openidcfg_url = "https://{0}.bff.cariad.digital/login/v1/idk/openid-configuration".format( @@ -772,7 +771,7 @@ async def login_request(self, user: str, password: str): self._authorizationServerBaseURLLive = "https://emea.bff.cariad.digital/login/v1/audi" if "authorizationServerBaseURLLive" in marketcfg_json: self._authorizationServerBaseURLLive = marketcfg_json[ - "authorizationServerBaseURLLive" + "myAudiAuthorizationServerProxyServiceURLProduction" ] self.mbbOAuthBaseURL = "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth" if "mbbOAuthBaseURLLive" in marketcfg_json: @@ -791,7 +790,7 @@ async def login_request(self, user: str, password: str): revocation_endpoint = ( "https://emea.bff.cariad.digital/login/v1/idk/revoke" ) - if revocation_endpoint in openidcfg_json: + if "revocation_endpoint" in openidcfg_json: revocation_endpoint = openidcfg_json["revocation_endpoint"] # generate code_challenge diff --git a/custom_components/audiconnect/device_tracker.py b/custom_components/audiconnect/device_tracker.py index 9da512ab..9b286f16 100644 --- a/custom_components/audiconnect/device_tracker.py +++ b/custom_components/audiconnect/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking an Audi.""" import logging -from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -79,7 +79,7 @@ def should_poll(self): @property def source_type(self): """Return the source type, eg gps or router, of the device.""" - return SourceType.GPS + return SOURCE_TYPE_GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/custom_components/audiconnect/manifest.json b/custom_components/audiconnect/manifest.json index b6b00008..990406a3 100644 --- a/custom_components/audiconnect/manifest.json +++ b/custom_components/audiconnect/manifest.json @@ -3,7 +3,6 @@ "name": "Audi Connect", "config_flow": true, "documentation": "https://github.com/arjenvrh/audi_connect_ha", - "integration_type": "hub", "issue_tracker": "https://github.com/arjenvrh/audi_connect_ha/issues", "requirements": ["beautifulsoup4"], "dependencies": [], diff --git a/custom_components/audiconnect/util.py b/custom_components/audiconnect/util.py index 770364d1..dfe12b64 100644 --- a/custom_components/audiconnect/util.py +++ b/custom_components/audiconnect/util.py @@ -28,12 +28,12 @@ def log_exception(exception, message): def parse_int(val: str): try: return int(val) - except ValueError: + except (ValueError, TypeError): return None def parse_float(val: str): try: return float(val) - except ValueError: + except (ValueError, TypeError): return None diff --git a/hacs.json b/hacs.json index 09ee7538..85105dab 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,5 @@ { "name": "Audi connect", + "domains": ["sensor", "binary_sensor", "switch", "device_tracker", "lock"], "homeassistant": "0.110.0" }