Skip to content

Commit

Permalink
partial update to new Audi API
Browse files Browse the repository at this point in the history
tested with Q8 e-tron
known limitations: setting climatisation and other setters fail
  • Loading branch information
t0bias-r committed Mar 1, 2024
1 parent dc6abae commit 77b0615
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 66 deletions.
6 changes: 3 additions & 3 deletions custom_components/audiconnect/audi_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
47 changes: 17 additions & 30 deletions custom_components/audiconnect/audi_connect_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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

Expand Down
165 changes: 149 additions & 16 deletions custom_components/audiconnect/audi_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")

Expand Down
23 changes: 11 additions & 12 deletions custom_components/audiconnect/audi_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(),
)
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions custom_components/audiconnect/device_tracker.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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."""
Expand Down
1 change: 0 additions & 1 deletion custom_components/audiconnect/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
Loading

0 comments on commit 77b0615

Please sign in to comment.