Repository: bimmerconnected/bimmer_connected
Pull request #580 (2023-11-11 21:24:22)

Add exception handling for unexpected responses
## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
Adds exception handling, hopefully fixing some of the issues our users see if the API doesn't respond with the correct vehicle details (e.g. tire status) or an additional, non-functioning vehicle in the account.

## Type of change
- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (which adds functionality to this library)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`b267df3`)](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/commit/b267df3bcaf58e6c0abd33e41d2b6aba9008816d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) 100.00% compared to head [(`cf50514`)](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) 100.00%.
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            master      #580   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         1382      1397   +15     
=========================================
+ Hits          1382      1397   +15     
```

| [Flag](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | Coverage Δ | |
|---|---|---|
| [3.10](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | `?` | |
| [3.11](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | `100.00% <100.00%> (?)` | |
| [3.12](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | `?` | |
| [3.8](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | `?` | |
| [3.9](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | Coverage Δ | |
|---|---|---|
| [bimmer\_connected/account.py](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected#diff-YmltbWVyX2Nvbm5lY3RlZC9hY2NvdW50LnB5) | `100.00% <100.00%> (ø)` | |
| [bimmer\_connected/vehicle/vehicle.py](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected#diff-YmltbWVyX2Nvbm5lY3RlZC92ZWhpY2xlL3ZlaGljbGUucHk=) | `100.00% <100.00%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/580?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3602601..18074a0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,12 @@
 repos:
   - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.0.282
+    rev: v0.1.5
     hooks:
       - id: ruff
         args:
           - --fix
   - repo: https://github.com/psf/black
-    rev: 23.7.0
+    rev: 23.11.0
     hooks:
       - id: black
         args:
@@ -14,7 +14,7 @@ repos:
           - --quiet
         files: ^(bimmer_connected/.+)?[^/]+\.py$
   - repo: https://github.com/codespell-project/codespell
-    rev: v2.2.5
+    rev: v2.2.6
     hooks:
       - id: codespell
         args:
@@ -24,7 +24,7 @@ repos:
         exclude_types: [csv, json]
         exclude: ^test/responses/
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.4.1
+    rev: v1.7.0
     hooks: 
       - id: mypy
         name: mypy
diff --git a/bimmer_connected/account.py b/bimmer_connected/account.py
index 9d46ff2..f948625 100644
--- a/bimmer_connected/account.py
+++ b/bimmer_connected/account.py
@@ -1,6 +1,7 @@
 """Access to a MyBMW account and all vehicles therein."""
 
 import datetime
+import json
 import logging
 from dataclasses import InitVar, dataclass, field
 from typing import List, Optional
@@ -17,7 +18,7 @@ from bimmer_connected.const import (
     VEHICLES_URL,
     CarBrands,
 )
-from bimmer_connected.models import AnonymizedResponse, GPSPosition
+from bimmer_connected.models import AnonymizedResponse, GPSPosition, MyBMWAPIError, MyBMWAuthError, MyBMWQuotaError
 from bimmer_connected.vehicle import MyBMWVehicle
 
 VALID_UNTIL_OFFSET = datetime.timedelta(seconds=10)
@@ -92,43 +93,59 @@ class MyBMWAccount:
             await self._init_vehicles()
 
         async with MyBMWClient(self.config) as client:
+            error_count = 0
             for vehicle in self.vehicles:
                 # Get the detailed vehicle state
-                state_response = await client.get(
-                    VEHICLE_STATE_URL,
-                    params={
-                        "apptimezone": self.utcdiff,
-                        "appDateTime": int(fetched_at.timestamp() * 1000),
-                    },
-                    headers={
-                        **client.generate_default_header(vehicle.brand),
-                        "bmw-vin": vehicle.vin,
-                    },
-                )
-                vehicle_state = state_response.json()
-
-                # Get detailed charging settings if supported by vehicle
-                charging_settings = None
-                if vehicle_state[ATTR_CAPABILITIES].get("isChargingPlanSupported", False) or vehicle_state[
-                    ATTR_CAPABILITIES
-                ].get("isChargingSettingsEnabled", False):
-                    charging_settings_response = await client.get(
-                        VEHICLE_CHARGING_DETAILS_URL,
+                try:
+                    state_response = await client.get(
+                        VEHICLE_STATE_URL,
                         params={
-                            "fields": "charging-profile",
-                            "has_charging_settings_capabilities": vehicle_state[ATTR_CAPABILITIES][
-                                "isChargingSettingsEnabled"
-                            ],
+                            "apptimezone": self.utcdiff,
+                            "appDateTime": int(fetched_at.timestamp() * 1000),
                         },
                         headers={
                             **client.generate_default_header(vehicle.brand),
-                            "bmw-current-date": fetched_at.isoformat(),
                             "bmw-vin": vehicle.vin,
                         },
                     )
-                    charging_settings = charging_settings_response.json()
-
-                self.add_vehicle(vehicle.data, vehicle_state, charging_settings, fetched_at)
+                    vehicle_state = state_response.json()
+
+                    # Get detailed charging settings if supported by vehicle
+                    charging_settings = None
+                    if vehicle_state[ATTR_CAPABILITIES].get("isChargingPlanSupported", False) or vehicle_state[
+                        ATTR_CAPABILITIES
+                    ].get("isChargingSettingsEnabled", False):
+                        charging_settings_response = await client.get(
+                            VEHICLE_CHARGING_DETAILS_URL,
+                            params={
+                                "fields": "charging-profile",
+                                "has_charging_settings_capabilities": vehicle_state[ATTR_CAPABILITIES][
+                                    "isChargingSettingsEnabled"
+                                ],
+                            },
+                            headers={
+                                **client.generate_default_header(vehicle.brand),
+                                "bmw-current-date": fetched_at.isoformat(),
+                                "bmw-vin": vehicle.vin,
+                            },
+                        )
+                        charging_settings = charging_settings_response.json()
+
+                    self.add_vehicle(vehicle.data, vehicle_state, charging_settings, fetched_at)
+                except (MyBMWAPIError, json.JSONDecodeError) as ex:
+                    # We don't want to fail completely if one vehicle fails, but we want to know about it
+                    error_count += 1
+
+                    # If it's a MyBMWQuotaError or MyBMWAuthError, we want to raise it
+                    if isinstance(ex, (MyBMWQuotaError, MyBMWAuthError)):
+                        raise ex
+
+                    # Always log the error
+                    _LOGGER.error("Unable to get details for vehicle %s - (%s) %s", vehicle.vin, type(ex).__name__, ex)
+
+                    # If all vehicles fail, we want to raise an exception
+                    if error_count == len(self.vehicles):
+                        raise ex
 
     def add_vehicle(
         self,
diff --git a/bimmer_connected/vehicle/vehicle.py b/bimmer_connected/vehicle/vehicle.py
index ae63139..e6b6ac7 100644
--- a/bimmer_connected/vehicle/vehicle.py
+++ b/bimmer_connected/vehicle/vehicle.py
@@ -115,11 +115,14 @@ class MyBMWVehicle:
             (Tires, "tires"),
         ]
         for cls, vehicle_attribute in update_entities:
-            if getattr(self, vehicle_attribute) is None:
-                setattr(self, vehicle_attribute, cls.from_vehicle_data(vehicle_data))
-            else:
-                curr_attr: "VehicleDataBase" = getattr(self, vehicle_attribute)
-                curr_attr.update_from_vehicle_data(vehicle_data)
+            try:
+                if getattr(self, vehicle_attribute) is None:
+                    setattr(self, vehicle_attribute, cls.from_vehicle_data(vehicle_data))
+                else:
+                    curr_attr: "VehicleDataBase" = getattr(self, vehicle_attribute)
+                    curr_attr.update_from_vehicle_data(vehicle_data)
+            except (KeyError, TypeError) as ex:
+                _LOGGER.warning("Unable to update %s - (%s) %s", vehicle_attribute, type(ex).__name__, ex)
 
     @staticmethod
     def combine_data(


Test patch:
diff --git a/bimmer_connected/tests/__init__.py b/bimmer_connected/tests/__init__.py
index 56e3653..39a7f5e 100644
--- a/bimmer_connected/tests/__init__.py
+++ b/bimmer_connected/tests/__init__.py
@@ -35,11 +35,16 @@ REMOTE_SERVICE_RESPONSE_ERROR = RESPONSE_DIR / "remote_services" / "eadrax_servi
 REMOTE_SERVICE_RESPONSE_EVENTPOSITION = RESPONSE_DIR / "remote_services" / "eadrax_service_eventposition.json"
 
 
-def get_fingerprint_count() -> int:
+def get_fingerprint_state_count() -> int:
     """Return number of loaded vehicles."""
     return sum([len(vehicles) for vehicles in ALL_VEHICLES.values()])
 
 
+def get_fingerprint_charging_settings_count() -> int:
+    """Return number of loaded vehicles."""
+    return len(ALL_CHARGING_SETTINGS)
+
+
 def load_response(path: Union[Path, str]) -> Any:
     """Load a stored response."""
     with open(path, "rb") as file:
diff --git a/bimmer_connected/tests/common.py b/bimmer_connected/tests/common.py
index 9fbf402..31d30ab 100644
--- a/bimmer_connected/tests/common.py
+++ b/bimmer_connected/tests/common.py
@@ -111,7 +111,7 @@ class MyBMWMockRouter(respx.MockRouter):
         """Add routes for vehicle requests."""
 
         self.get("/eadrax-vcs/v4/vehicles").mock(side_effect=self.vehicles_sideeffect)
-        self.get("/eadrax-vcs/v4/vehicles/state").mock(side_effect=self.vehicle_state_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)
 
     def add_remote_service_routes(self) -> None:
diff --git a/bimmer_connected/tests/conftest.py b/bimmer_connected/tests/conftest.py
index 7be1ea0..49c5329 100644
--- a/bimmer_connected/tests/conftest.py
+++ b/bimmer_connected/tests/conftest.py
@@ -1,11 +1,13 @@
 """Fixtures for BMW tests."""
-from typing import Generator, Optional
+from collections import deque
+from typing import Deque, Generator, Optional
 
 import pytest
 import respx
 
 from bimmer_connected.account import MyBMWAccount
 from bimmer_connected.const import Regions
+from bimmer_connected.models import AnonymizedResponse
 
 from . import (
     ALL_CHARGING_SETTINGS,
@@ -31,6 +33,14 @@ def bmw_fixture(request: pytest.FixtureRequest) -> Generator[respx.MockRouter, N
         yield router
 
 
+@pytest.fixture
+def bmw_log_all_responses(monkeypatch: pytest.MonkeyPatch):
+    """Increase the length of the response store to log all responses."""
+    temp_store: Deque[AnonymizedResponse] = deque(maxlen=100)
+    monkeypatch.setattr("bimmer_connected.api.client.RESPONSE_STORE", temp_store)
+    monkeypatch.setattr("bimmer_connected.account.RESPONSE_STORE", temp_store)
+
+
 async def prepare_account_with_vehicles(region: Optional[Regions] = None, metric: bool = True):
     """Initialize account and get vehicles."""
     account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, region or TEST_REGION, use_metric_units=metric)
diff --git a/bimmer_connected/tests/test_account.py b/bimmer_connected/tests/test_account.py
index 0019b87..d9ea4e7 100644
--- a/bimmer_connected/tests/test_account.py
+++ b/bimmer_connected/tests/test_account.py
@@ -23,7 +23,9 @@ from . import (
     TEST_REGION_STRING,
     TEST_USERNAME,
     VIN_G26,
-    get_fingerprint_count,
+    VIN_I20,
+    get_fingerprint_charging_settings_count,
+    get_fingerprint_state_count,
     load_response,
 )
 from .conftest import prepare_account_with_vehicles
@@ -175,7 +177,7 @@ async def test_vehicles(bmw_fixture: respx.Router):
     await account.get_vehicles()
 
     assert account.config.authentication.access_token is not None
-    assert get_fingerprint_count() == len(account.vehicles)
+    assert get_fingerprint_state_count() == len(account.vehicles)
 
     vehicle = account.get_vehicle(VIN_G26)
     assert vehicle is not None
@@ -196,15 +198,15 @@ async def test_vehicle_init(bmw_fixture: respx.Router):
 
         # First call on init
         await account.get_vehicles()
-        assert len(account.vehicles) == get_fingerprint_count()
+        assert len(account.vehicles) == get_fingerprint_state_count()
 
         # No call to _init_vehicles()
         await account.get_vehicles()
-        assert len(account.vehicles) == get_fingerprint_count()
+        assert len(account.vehicles) == get_fingerprint_state_count()
 
         # Second, forced call _init_vehicles()
         await account.get_vehicles(force_init=True)
-        assert len(account.vehicles) == get_fingerprint_count()
+        assert len(account.vehicles) == get_fingerprint_state_count()
 
         assert mock_listener.call_count == 2
 
@@ -254,23 +256,42 @@ async def test_vehicle_search_case(bmw_fixture: respx.Router):
 
 
 @pytest.mark.asyncio
-async def test_get_fingerprints(bmw_fixture: respx.Router):
+async def test_get_fingerprints(monkeypatch: pytest.MonkeyPatch, bmw_fixture: respx.Router, bmw_log_all_responses):
     """Test getting fingerprints."""
+
+    # Prepare Number of good responses (vehicle states, charging settings per vehicle)
+    # and 2x vehicle list
+    json_count = get_fingerprint_state_count() + get_fingerprint_charging_settings_count() + 2
+
     account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True)
     await account.get_vehicles()
 
-    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state").respond(
+    # This should have been successful
+    filenames = [Path(f.filename) for f in account.get_stored_responses()]
+    json_files = [f for f in filenames if f.suffix == ".json"]
+    txt_files = [f for f in filenames if f.suffix == ".txt"]
+
+    assert len(json_files) == json_count  # all good
+    assert len(txt_files) == 0  # no errors
+
+    # Now we simulate an error for a single vehicle
+    # We need to remove the existing state route first and add it back later as otherwise our error call is never
+    # matched (respx matches by order of routes and we don't replace the existing one)
+    state_route = bmw_fixture.routes.pop("state")
+    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state", headers={"bmw-vin": VIN_G26}).respond(
         500, text=load_response(RESPONSE_DIR / "auth" / "auth_error_internal_error.txt")
     )
-    with pytest.raises(MyBMWAPIError):
-        await account.get_vehicles()
+    bmw_fixture.routes.add(state_route, "state")
+
+    account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True)
+    await account.get_vehicles()
 
     filenames = [Path(f.filename) for f in account.get_stored_responses()]
     json_files = [f for f in filenames if f.suffix == ".json"]
     txt_files = [f for f in filenames if f.suffix == ".txt"]
 
-    assert len(json_files) == (get_fingerprint_count() + 1)
-    assert len(txt_files) == 1
+    assert len(json_files) == json_count - 2  # missing on 1 state and 1 charging setting
+    assert len(txt_files) == 1  # error message from state, charging setting was not loaded anymore
 
 
 @pytest.mark.asyncio
@@ -473,6 +494,51 @@ async def test_403_quota_exceeded_vehicles_usa(caplog, bmw_fixture: respx.Router
     assert len(log_quota) == 1
 
 
+@pytest.mark.asyncio
+async def test_incomplete_vehicle_details(caplog, bmw_fixture: respx.Router):
+    """Test incorrect responses for vehicle details."""
+    account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+    # get vehicles once
+    await account.get_vehicles()
+
+    # We need to remove the existing state route first and add it back later as otherwise our error call is never
+    # matched (respx matches by order of routes and we don't replace the existing one)
+    state_route = bmw_fixture.routes.pop("state")
+    # JSON, but error
+    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state", headers={"bmw-vin": VIN_I20}).respond(
+        500, json={"statusCode": 500, "message": "Something is broken."}
+    )
+    # No JSON
+    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state", headers={"bmw-vin": VIN_G26}).respond(
+        500, text=load_response(RESPONSE_DIR / "auth" / "auth_error_internal_error.txt")
+    )
+    bmw_fixture.routes.add(state_route, "state")
+
+    await account.get_vehicles()
+
+    log_error = [r for r in caplog.records if "Unable to get details" in r.message]
+    assert len(log_error) == 2
+
+
+@pytest.mark.asyncio
+async def test_no_vehicle_details(caplog, bmw_fixture: respx.Router):
+    """Test raising an exception if no responses for vehicle details are received."""
+    account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+    await account.get_vehicles()
+
+    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state").mock(
+        return_value=httpx.Response(
+            500,
+            json={"statusCode": 500, "message": "Something is broken."},
+        )
+    )
+    with pytest.raises(MyBMWAPIError):
+        await account.get_vehicles()
+
+    log_error = [r for r in caplog.records if "Unable to get details" in r.message]
+    assert len(log_error) == get_fingerprint_state_count()
+
+
 @pytest.mark.asyncio
 async def test_client_async_only(bmw_fixture: respx.Router):
     """Test that the Authentication providers only work async."""
diff --git a/bimmer_connected/tests/test_api.py b/bimmer_connected/tests/test_api.py
index dd1967b..d36593e 100644
--- a/bimmer_connected/tests/test_api.py
+++ b/bimmer_connected/tests/test_api.py
@@ -7,10 +7,19 @@ import respx
 from bimmer_connected.account import MyBMWAccount
 from bimmer_connected.api.regions import get_region_from_name, valid_regions
 from bimmer_connected.api.utils import anonymize_data
-from bimmer_connected.models import MyBMWAPIError
 from bimmer_connected.utils import log_response_store_to_file
 
-from . import RESPONSE_DIR, TEST_PASSWORD, TEST_REGION, TEST_USERNAME, get_fingerprint_count, load_response
+from . import (
+    RESPONSE_DIR,
+    TEST_PASSWORD,
+    TEST_REGION,
+    TEST_USERNAME,
+    VIN_G26,
+    get_fingerprint_charging_settings_count,
+    get_fingerprint_state_count,
+    # get_fingerprint_count,
+    load_response,
+)
 
 
 def test_valid_regions():
@@ -54,16 +63,19 @@ def test_anonymize_data():
 
 
 @pytest.mark.asyncio
-async def test_storing_fingerprints(tmp_path, bmw_fixture: respx.Router):
+async def test_storing_fingerprints(tmp_path, bmw_fixture: respx.Router, bmw_log_all_responses):
     """Test storing fingerprints to file."""
-    account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True)
-    await account.get_vehicles()
 
-    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state").respond(
+    # We need to remove the existing state route first and add it back later as otherwise our error call is never
+    # matched (respx matches by order of routes and we don't replace the existing one)
+    state_route = bmw_fixture.routes.pop("state")
+    bmw_fixture.get("/eadrax-vcs/v4/vehicles/state", headers={"bmw-vin": VIN_G26}).respond(
         500, text=load_response(RESPONSE_DIR / "auth" / "auth_error_internal_error.txt")
     )
-    with pytest.raises(MyBMWAPIError):
-        await account.get_vehicles()
+    bmw_fixture.routes.add(state_route, "state")
+
+    account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True)
+    await account.get_vehicles()
 
     log_response_store_to_file(account.get_stored_responses(), tmp_path)
 
@@ -71,14 +83,23 @@ async def test_storing_fingerprints(tmp_path, bmw_fixture: respx.Router):
     json_files = [f for f in files if f.suffix == ".json"]
     txt_files = [f for f in files if f.suffix == ".txt"]
 
-    assert len(json_files) == (get_fingerprint_count() + 1)
+    assert len(json_files) == (get_fingerprint_state_count() + get_fingerprint_charging_settings_count() + 2 - 2)
     assert len(txt_files) == 1
 
 
 @pytest.mark.asyncio
-async def test_fingerprint_deque(bmw_fixture: respx.Router):
+async def test_fingerprint_deque(monkeypatch: pytest.MonkeyPatch, bmw_fixture: respx.Router):
     """Test storing fingerprints to file."""
-    # with bmw_fixture as mock_api:
+    # Increase length of response store for local testing
+
+    # temp_store = deque(maxlen=100)
+    # monkeypatch.setattr("bimmer_connected.api.client.RESPONSE_STORE", temp_store)
+    # monkeypatch.setattr("bimmer_connected.account.RESPONSE_STORE", temp_store)
+
+    # Prepare Number of good responses (vehicle states, charging settings per vehicle)
+    # # and 2x vehicle list
+    # json_count = get_fingerprint_state_count() + get_fingerprint_charging_settings_count() + 2
+
     account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True)
     await account.get_vehicles()
     await account.get_vehicles()
diff --git a/bimmer_connected/tests/test_vehicle_status.py b/bimmer_connected/tests/test_vehicle_status.py
index ff20ab6..65406a3 100644
--- a/bimmer_connected/tests/test_vehicle_status.py
+++ b/bimmer_connected/tests/test_vehicle_status.py
@@ -7,7 +7,7 @@ import respx
 import time_machine
 
 from bimmer_connected.api.regions import get_region_from_name
-from bimmer_connected.vehicle.climate import ClimateActivityState
+from bimmer_connected.vehicle.climate import Climate, ClimateActivityState
 from bimmer_connected.vehicle.doors_windows import LidState, LockState
 from bimmer_connected.vehicle.fuel_and_battery import ChargingState, FuelAndBattery
 from bimmer_connected.vehicle.location import VehicleLocation
@@ -45,6 +45,40 @@ async def test_generic(caplog, bmw_fixture: respx.Router):
     assert len(get_deprecation_warning_count(caplog)) == 0
 
 
+@pytest.mark.asyncio
+async def test_generic_error_handling(caplog, bmw_fixture: respx.Router):
+    """Test error handling when vehicle is set up."""
+    account = await prepare_account_with_vehicles()
+
+    vehicle = account.get_vehicle(VIN_G26)
+    state_wo_front_left = vehicle.data["state"].copy()
+    state_wo_front_left["tireState"]["frontLeft"].pop("status", None)
+    vehicle.tires = None
+
+    assert vehicle.tires is None
+    vehicle.update_state(vehicle.data, state_wo_front_left)
+    assert (
+        any("tires" in r.message and "TypeError" in r.message and "'status'" in r.message for r in caplog.records)
+        is True
+    )
+    assert vehicle.tires is None
+
+    caplog.clear()
+
+    vehicle = account.get_vehicle(VIN_I20)
+    state_wo_climate_activity = vehicle.data["state"].copy()
+    state_wo_climate_activity["climateControlState"].pop("activity", None)
+    vehicle.climate = Climate(account_timezone=account.timezone)
+
+    assert vehicle.climate.activity == ClimateActivityState.UNKNOWN
+    vehicle.update_state(vehicle.data, state_wo_climate_activity)
+    assert (
+        any("climate" in r.message and "KeyError" in r.message and "'activity'" in r.message for r in caplog.records)
+        is True
+    )
+    assert vehicle.climate.activity == ClimateActivityState.UNKNOWN
+
+
 @pytest.mark.asyncio
 async def test_range_combustion_no_info(caplog, bmw_fixture: respx.Router):
     """Test if the parsing of mileage and range is working."""