Skip to content

Commit

Permalink
Adjust URLs (#591)
Browse files Browse the repository at this point in the history
* Update to 3.11.1

* Update tests

* Fix get_vehicle_image request

* Update calculation of fingerprint count in tests
  • Loading branch information
rikroe authored Feb 11, 2024
1 parent 8498511 commit 5d78330
Show file tree
Hide file tree
Showing 40 changed files with 552 additions and 516 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.5
rev: v0.1.14
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/psf/black
rev: 23.11.0
rev: 23.12.1
hooks:
- id: black
args:
Expand All @@ -24,7 +24,7 @@ repos:
exclude_types: [csv, json]
exclude: ^test/responses/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.0
rev: v1.8.0
hooks:
- id: mypy
name: mypy
Expand Down
28 changes: 17 additions & 11 deletions bimmer_connected/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
from dataclasses import InitVar, dataclass, field
from typing import List, Optional

import httpx

from bimmer_connected.api.authentication import MyBMWAuthentication
from bimmer_connected.api.client import RESPONSE_STORE, MyBMWClient, MyBMWClientConfiguration
from bimmer_connected.api.regions import Regions
from bimmer_connected.const import (
ATTR_ATTRIBUTES,
ATTR_CAPABILITIES,
VEHICLE_CHARGING_DETAILS_URL,
VEHICLE_PROFILE_URL,
VEHICLE_STATE_URL,
VEHICLES_URL,
CarBrands,
Expand Down Expand Up @@ -76,18 +76,24 @@ async def _init_vehicles(self) -> None:
fetched_at = datetime.datetime.now(datetime.timezone.utc)

async with MyBMWClient(self.config) as client:
vehicles_responses: List[httpx.Response] = [
await client.get(
for brand in CarBrands:
request_headers = client.generate_default_header(brand)
vehicle_list_response = await client.post(
VEHICLES_URL,
headers={
**client.generate_default_header(brand),
},
headers=request_headers,
)
for brand in CarBrands
]

for response in vehicles_responses:
for vehicle_base in response.json():
for vehicle in vehicle_list_response.json()["mappingInfos"]:
vehicle_profile_response = await client.get(
VEHICLE_PROFILE_URL, headers=dict(request_headers, **{"bmw-vin": vehicle["vin"]})
)
vehicle_profile = vehicle_profile_response.json()

vehicle_base = dict(
{ATTR_ATTRIBUTES: {k: v for k, v in vehicle_profile.items() if k != "vin"}},
**{"vin": vehicle_profile["vin"]}
)

self.add_vehicle(vehicle_base, None, None, fetched_at)

async def get_vehicles(self, force_init: bool = False) -> None:
Expand Down
5 changes: 3 additions & 2 deletions bimmer_connected/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,17 @@ async def _login_row_na(self):
code_challenge = create_s256_code_challenge(code_verifier)

state = generate_token(22)
nonce = generate_token(22)

# Set up authenticate endpoint
authenticate_url = oauth_settings["tokenEndpoint"].replace("/token", "/authenticate")
oauth_base_values = {
"client_id": oauth_settings["clientId"],
"response_type": "code",
"scope": " ".join(oauth_settings["scopes"]),
"redirect_uri": oauth_settings["returnUrl"],
"state": state,
"nonce": "login_nonce",
"scope": " ".join(oauth_settings["scopes"]),
"nonce": nonce,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
}
Expand Down
3 changes: 2 additions & 1 deletion bimmer_connected/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ def generate_default_header(self, brand: Optional[CarBrands] = None) -> Dict[str
return {
"accept": "application/json",
"accept-language": "en",
"x-raw-locale": "en-US",
"user-agent": get_user_agent(self.config.authentication.region),
"x-user-agent": X_USER_AGENT.format(
brand=(brand or CarBrands.BMW).value,
app_version=get_app_version(self.config.authentication.region),
region=self.config.authentication.region.value,
),
**get_correlation_id(),
"bmw-units-preferences": "d=KM;v=L",
"bmw-units-preferences": "d=KM;v=L;p=B;ec=KWH100KM;fc=L100KM;em=GKM;",
"24-hour-format": "true",
}
19 changes: 10 additions & 9 deletions bimmer_connected/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ class Regions(str, Enum):
}

APP_VERSIONS = {
Regions.NORTH_AMERICA: "3.9.0(27760)",
Regions.REST_OF_WORLD: "3.9.0(27760)",
Regions.CHINA: "3.6.1(23634)",
Regions.NORTH_AMERICA: "3.11.1(29513)",
Regions.REST_OF_WORLD: "3.11.1(29513)",
Regions.CHINA: "3.11.1(29513)",
}

HTTPX_TIMEOUT = 30.0

USER_AGENTS = {
Regions.NORTH_AMERICA: "Dart/2.19 (dart:io)",
Regions.REST_OF_WORLD: "Dart/2.19 (dart:io)",
Regions.CHINA: "Dart/2.18 (dart:io)",
Regions.NORTH_AMERICA: "Dart/3.0 (dart:io)",
Regions.REST_OF_WORLD: "Dart/3.0 (dart:io)",
Regions.CHINA: "Dart/3.0 (dart:io)",
}
X_USER_AGENT = "android(TQ2A.230405.003.B2);{brand};{app_version};{region}"

Expand All @@ -60,8 +60,9 @@ class Regions(str, Enum):

OAUTH_CONFIG_URL = "/eadrax-ucs/v1/presentation/oauth/config"

VEHICLES_URL = "/eadrax-vcs/v4/vehicles"
VEHICLE_STATE_URL = VEHICLES_URL + "/state"
VEHICLES_URL = "/eadrax-vcs/v5/vehicle-list"
VEHICLE_PROFILE_URL = "/eadrax-vcs/v5/vehicle-data/profile"
VEHICLE_STATE_URL = "/eadrax-vcs/v4/vehicles/state"

REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v3/presentation/remote-commands"
REMOTE_SERVICE_URL = REMOTE_SERVICE_BASE_URL + "/{vin}/{service_type}"
Expand All @@ -74,7 +75,7 @@ class Regions(str, Enum):
VEHICLE_CHARGING_PROFILE_SET_URL = VEHICLE_CHARGING_BASE_URL + "/charging-profile"
VEHICLE_CHARGING_START_STOP_URL = VEHICLE_CHARGING_BASE_URL + "/{service_type}"

VEHICLE_IMAGE_URL = "/eadrax-ics/v3/presentation/vehicles/{vin}/images?carView={view}"
VEHICLE_IMAGE_URL = "/eadrax-ics/v5/presentation/vehicles/images"
VEHICLE_POI_URL = "/eadrax-dcs/v1/send-to-car/send-to-car"

VEHICLE_CHARGING_STATISTICS_URL = "/eadrax-chs/v1/charging-statistics"
Expand Down
36 changes: 24 additions & 12 deletions bimmer_connected/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
from pathlib import Path
from typing import Any, Dict, List, Union
from typing import Any, Dict, Union

from bimmer_connected.api.regions import Regions
from bimmer_connected.const import CarBrands
Expand All @@ -23,7 +23,8 @@
VIN_I01_REX = "WBY00000000REXI01"
VIN_I20 = "WBA00000000DEMO01"

ALL_VEHICLES: Dict[str, List[Dict]] = {brand.value: [] for brand in CarBrands}
ALL_VEHICLES: Dict[str, Dict] = {brand.value: {} for brand in CarBrands}
ALL_PROFILES: Dict[str, Dict] = {}
ALL_STATES: Dict[str, Dict] = {}
ALL_CHARGING_SETTINGS: Dict[str, Dict] = {}

Expand All @@ -35,14 +36,18 @@
REMOTE_SERVICE_RESPONSE_EVENTPOSITION = RESPONSE_DIR / "remote_services" / "eadrax_service_eventposition.json"


def get_fingerprint_state_count() -> int:
"""Return number of loaded vehicles."""
return sum([len(vehicles) for vehicles in ALL_VEHICLES.values()])
def get_fingerprint_count(type: str) -> int:
"""Return number of requests/fingerprints for a given type."""


def get_fingerprint_charging_settings_count() -> int:
"""Return number of loaded vehicles."""
return len(ALL_CHARGING_SETTINGS)
if type == "vehicles":
return len(CarBrands)
if type == "states":
return len(ALL_STATES)
if type == "profiles":
return len(ALL_PROFILES)
if type == "charging_settings":
return len(ALL_CHARGING_SETTINGS)
return 0


def load_response(path: Union[Path, str]) -> Any:
Expand All @@ -53,10 +58,17 @@ def load_response(path: Union[Path, str]) -> Any:
return file.read().decode("UTF-8")


for fingerprint in RESPONSE_DIR.rglob("*-eadrax-vcs_v4_vehicles.json"):
for fingerprint in RESPONSE_DIR.rglob("*-eadrax-vcs_v5_vehicle-list.json"):
brand = fingerprint.stem.split("-")[0]
for vehicle in load_response(fingerprint):
ALL_VEHICLES[brand].append(vehicle)
response = load_response(fingerprint)

if ALL_VEHICLES[brand].get("mappingInfos"):
ALL_VEHICLES[brand]["mappingInfos"].extend(response["mappingInfos"])
else:
ALL_VEHICLES[brand] = response

for profile in RESPONSE_DIR.rglob("*-eadrax-vcs_v5_vehicle-data_profile_*.json"):
ALL_PROFILES[profile.stem.split("_")[-1]] = load_response(profile)

for state in RESPONSE_DIR.rglob("*-eadrax-vcs_v4_vehicles_state_*.json"):
ALL_STATES[state.stem.split("_")[-1]] = load_response(state)
Expand Down
23 changes: 19 additions & 4 deletions bimmer_connected/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ class MyBMWMockRouter(respx.MockRouter):
def __init__(
self,
vehicles_to_load: Optional[List[str]] = None,
profiles: Optional[Dict[str, Dict]] = None,
states: Optional[Dict[str, Dict]] = None,
charging_settings: Optional[Dict[str, Dict]] = None,
) -> None:
"""Initialize the MyBMWMockRouter with clean responses."""
super().__init__(assert_all_called=False)
self.vehicles_to_load = vehicles_to_load or []
self.profiles = deepcopy(profiles) if profiles else {}
self.states = deepcopy(states) if states else {}
self.charging_settings = deepcopy(charging_settings) if charging_settings else {}

Expand Down Expand Up @@ -110,7 +112,8 @@ def add_login_routes(self) -> None:
def add_vehicle_routes(self) -> None:
"""Add routes for vehicle requests."""

self.get("/eadrax-vcs/v4/vehicles").mock(side_effect=self.vehicles_sideeffect)
self.post("/eadrax-vcs/v5/vehicle-list").mock(side_effect=self.vehicles_sideeffect)
self.get("/eadrax-vcs/v5/vehicle-data/profile").mock(side_effect=self.vehicle_profile_sideeffect)
self.get("/eadrax-vcs/v4/vehicles/state", name="state").mock(side_effect=self.vehicle_state_sideeffect)
self.get("/eadrax-crccs/v2/vehicles").mock(side_effect=self.vehicle_charging_settings_sideeffect)

Expand Down Expand Up @@ -171,15 +174,27 @@ def vehicles_sideeffect(self, request: httpx.Request) -> httpx.Response:
# Test if given region is valid
_ = Regions(x_user_agent[3])

fingerprints = ALL_VEHICLES.get(brand, [])
fingerprints = deepcopy(ALL_VEHICLES.get(brand, {"mappingInfos": []}))
if self.vehicles_to_load:
fingerprints = [f for f in fingerprints if f["vin"] in self.vehicles_to_load]
fingerprints["mappingInfos"] = [
f for f in fingerprints["mappingInfos"] if f["vin"] in self.vehicles_to_load
]

# Ensure order
fingerprints = sorted(fingerprints, key=lambda v: v["vin"])
fingerprints["mappingInfos"] = sorted(fingerprints["mappingInfos"], key=lambda v: v["vin"])

return httpx.Response(200, json=fingerprints)

def vehicle_profile_sideeffect(self, request: httpx.Request) -> httpx.Response:
"""Return /vehicle-data/profile response based on vin."""
x_user_agent = request.headers.get("x-user-agent", "").split(";")
assert len(x_user_agent) == 4

try:
return httpx.Response(200, json=self.profiles[request.headers["bmw-vin"]])
except KeyError:
return httpx.Response(404)

def vehicle_state_sideeffect(self, request: httpx.Request) -> httpx.Response:
"""Return /vehicles response based on x-user-agent."""
x_user_agent = request.headers.get("x-user-agent", "").split(";")
Expand Down
2 changes: 2 additions & 0 deletions bimmer_connected/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from . import (
ALL_CHARGING_SETTINGS,
ALL_PROFILES,
ALL_STATES,
TEST_PASSWORD,
TEST_REGION,
Expand All @@ -25,6 +26,7 @@ def bmw_fixture(request: pytest.FixtureRequest) -> Generator[respx.MockRouter, N
# Now we can start patching the API calls
router = MyBMWMockRouter(
vehicles_to_load=getattr(request, "param", []),
profiles=ALL_PROFILES,
states=ALL_STATES,
charging_settings=ALL_CHARGING_SETTINGS,
)
Expand Down

This file was deleted.

Loading

0 comments on commit 5d78330

Please sign in to comment.