Repository: bimmerconnected/bimmer_connected
Pull request #568 (2023-09-20 10:40:20)

Add headunit data class also exposing current vehicle software version
## Proposed change
This enhancement adds headunit information like ID Drive generation and its current software revision.
Only vehicle data that also exists on older vehicles was used.

```Headunit(idrive_version='ID8', headunit_type='MGU', software_version='07/2023.35')```

## Type of change
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] 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

## Checklist
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works.


rikroe: Need to double check, there seems to be something wrong with all pytests (maybe a test dependency upgrade, not your code l. 
cubinet-code: Thanks for having a look. 

It seem to be related to this: https://github.com/adamchainz/time-machine/issues/387 in test_get_remote_position_too_old.

```
diff --git a/requirements-test.txt b/requirements-test.txt
-time_machine
+time_machine==2.12.0
```

After going back to 2.12.0 version tests pass again:
```
============================================================================ test session starts ============================================================================
platform darwin -- Python 3.11.3, pytest-7.4.2, pluggy-1.3.0
rootdir: /Users/oliverf/Documents/Project/bimmer_connected
configfile: pyproject.toml
plugins: anyio-4.0.0, respx-0.20.2, timeout-2.1.0, cov-4.1.0, asyncio-0.21.1, time-machine-2.12.0
asyncio: mode=Mode.STRICT
collected 85 items                                                                                                                                                          

bimmer_connected/tests/test_account.py ..........................                                                                                                     [ 30%]
bimmer_connected/tests/test_api.py .....                                                                                                                              [ 36%]
bimmer_connected/tests/test_remote_services.py ................                                                                                                       [ 55%]
bimmer_connected/tests/test_utils.py .....                                                                                                                            [ 61%]
bimmer_connected/tests/test_vehicle.py ............                                                                                                                   [ 75%]
bimmer_connected/tests/test_vehicle_status.py .....................                                                                                                   [100%]

============================================================================ 85 passed in 8.20s =============================================================================


rikroe: Thanks for the hint! In that case, I would be fine with pinning `time_machine<2.13.0` for now.

cubinet-code: I submitted a PR, so you don't have to :)
rikroe: > I submitted a PR, so you don't have to :)

Thanks, please rebase and this should be good to go. 
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) Report
Patch coverage: **`100.00%`** and no project coverage change.
> Comparison is base [(`1376e3a`)](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/commit/1376e3ac3879e2aacc0e7a82f86c6ea5b34e11eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) 100.00% compared to head [(`022ba45`)](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) 100.00%.

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


```diff
@@            Coverage Diff            @@
##            master      #568   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         1334      1355   +21     
=========================================
+ Hits          1334      1355   +21     
```

| [Flag](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568/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/568/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.11](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568/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/568/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/568/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.9](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568/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%> (ø)` | |

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 Changed](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected) | Coverage Δ | |
|---|---|---|
| [bimmer\_connected/vehicle/reports.py](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bimmerconnected#diff-YmltbWVyX2Nvbm5lY3RlZC92ZWhpY2xlL3JlcG9ydHMucHk=) | `100.00% <100.00%> (ø)` | |
| [bimmer\_connected/vehicle/vehicle.py](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?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%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bimmerconnected/bimmer_connected/pull/568?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/bimmer_connected/vehicle/reports.py b/bimmer_connected/vehicle/reports.py
index 8b12290..0d6a174 100644
--- a/bimmer_connected/vehicle/reports.py
+++ b/bimmer_connected/vehicle/reports.py
@@ -5,7 +5,7 @@ import logging
 from dataclasses import dataclass, field
 from typing import Any, Dict, List, Optional
 
-from bimmer_connected.const import ATTR_STATE
+from bimmer_connected.const import ATTR_ATTRIBUTES, ATTR_STATE
 from bimmer_connected.models import StrEnum, ValueWithUnit, VehicleDataBase
 from bimmer_connected.utils import parse_datetime
 
@@ -38,7 +38,7 @@ class ConditionBasedService:
         dateTime: Optional[str] = None,
         mileage: Optional[int] = None,
         is_metric: bool = True,
-        **kwargs
+        **kwargs,
     ):
         """Parse a condition based service entry from the API format to `ConditionBasedService`."""
         due_distance = ValueWithUnit(mileage, "km" if is_metric else "mi") if mileage else ValueWithUnit(None, None)
@@ -117,3 +117,35 @@ class CheckControlMessageReport(VehicleDataBase):
             retval["has_check_control_messages"] = len([m for m in retval["messages"] if m.state != "LOW"]) > 0
 
         return retval
+
+
+@dataclass
+class Headunit(VehicleDataBase):
+    """Parse and summarizes headunit hard/software versions."""
+
+    idrive_version: str = ""
+    """IDRIVE generation."""
+
+    headunit_type: str = ""
+    """Type of headunit."""
+
+    software_version: str = ""
+    """Current software revision of vehicle"""
+
+    @classmethod
+    def _parse_vehicle_data(cls, vehicle_data: Dict) -> Optional[Dict]:
+        """Parse headunit hard/software."""
+        retval: Dict[str, Any] = {}
+
+        if ATTR_ATTRIBUTES in vehicle_data and "softwareVersionCurrent" in vehicle_data[ATTR_ATTRIBUTES]:
+            retval["idrive_version"] = vehicle_data[ATTR_ATTRIBUTES]["hmiVersion"]
+            retval["headunit_type"] = vehicle_data[ATTR_ATTRIBUTES]["headUnitType"]
+
+            istep = vehicle_data[ATTR_ATTRIBUTES]["softwareVersionCurrent"]["iStep"]
+            month = vehicle_data[ATTR_ATTRIBUTES]["softwareVersionCurrent"]["puStep"]["month"]
+            year = vehicle_data[ATTR_ATTRIBUTES]["softwareVersionCurrent"]["puStep"]["year"]
+            model_year = vehicle_data[ATTR_ATTRIBUTES]["year"]
+
+            retval["software_version"] = f"{month:02d}/{str(model_year)[:2]}{year:02d}.{str(istep)[1:]}"
+
+        return retval
diff --git a/bimmer_connected/vehicle/vehicle.py b/bimmer_connected/vehicle/vehicle.py
index 176faad..ae63139 100644
--- a/bimmer_connected/vehicle/vehicle.py
+++ b/bimmer_connected/vehicle/vehicle.py
@@ -21,7 +21,7 @@ from bimmer_connected.vehicle.doors_windows import DoorsAndWindows
 from bimmer_connected.vehicle.fuel_and_battery import FuelAndBattery
 from bimmer_connected.vehicle.location import VehicleLocation
 from bimmer_connected.vehicle.remote_services import RemoteServices
-from bimmer_connected.vehicle.reports import CheckControlMessageReport, ConditionBasedServiceReport
+from bimmer_connected.vehicle.reports import CheckControlMessageReport, ConditionBasedServiceReport, Headunit
 from bimmer_connected.vehicle.tires import Tires
 
 if TYPE_CHECKING:
@@ -84,6 +84,7 @@ class MyBMWVehicle:
         self.vehicle_location: VehicleLocation = VehicleLocation(account_region=account.region)
         self.doors_and_windows: DoorsAndWindows = DoorsAndWindows()
         self.condition_based_services: ConditionBasedServiceReport = ConditionBasedServiceReport()
+        self.headunit: Headunit = Headunit()
         self.check_control_messages: CheckControlMessageReport = CheckControlMessageReport()
         self.climate: Climate = Climate(account_timezone=account.timezone)
         self.charging_profile: Optional[ChargingProfile] = None
@@ -108,6 +109,7 @@ class MyBMWVehicle:
             (DoorsAndWindows, "doors_and_windows"),
             (ConditionBasedServiceReport, "condition_based_services"),
             (CheckControlMessageReport, "check_control_messages"),
+            (Headunit, "headunit"),
             (Climate, "climate"),
             (ChargingProfile, "charging_profile"),
             (Tires, "tires"),


Test patch:
diff --git a/bimmer_connected/tests/test_vehicle.py b/bimmer_connected/tests/test_vehicle.py
index fc001ab..38b7370 100644
--- a/bimmer_connected/tests/test_vehicle.py
+++ b/bimmer_connected/tests/test_vehicle.py
@@ -280,3 +280,22 @@ def test_gpsposition():
     assert pos == (1, 2)
     assert pos != "(1, 2)"
     assert pos[0] == 1
+
+
+@pytest.mark.asyncio
+async def test_headunit_data(caplog, bmw_fixture: respx.Router):
+    """Test if the parsing of headunit is working."""
+
+    status = (await prepare_account_with_vehicles()).get_vehicle(VIN_I20).headunit
+
+    assert status.idrive_version == "ID8"
+    assert status.headunit_type == "MGU"
+    assert status.software_version == "07/2021.00"
+
+    status = (await prepare_account_with_vehicles()).get_vehicle(VIN_F31).headunit
+
+    assert status.idrive_version == "ID4"
+    assert status.headunit_type == "NBT"
+    assert status.software_version == "11/2013.02"
+
+    assert len(get_deprecation_warning_count(caplog)) == 0