Repository: bimmerconnected/bimmer_connected
Pull request #713 (2024-12-25 09:49:09)

Get next service by distance/time
## 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 additional attributes to condition based services (next service by distance and time) as well as as string representation of current check control messages.

Both are for use within specific HA sensors.

## 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

## 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.



Patch:
diff --git a/bimmer_connected/vehicle/reports.py b/bimmer_connected/vehicle/reports.py
index 0598a54..2d81d93 100644
--- a/bimmer_connected/vehicle/reports.py
+++ b/bimmer_connected/vehicle/reports.py
@@ -55,6 +55,12 @@ class ConditionBasedServiceReport(VehicleDataBase):
     is_service_required: bool = False
     """Indicate if a service is required."""
 
+    next_service_by_distance: Optional[ConditionBasedService] = None
+    """Next service by distance."""
+
+    next_service_by_time: Optional[ConditionBasedService] = None
+    """Next service by due date."""
+
     @classmethod
     def _parse_vehicle_data(cls, vehicle_data: Dict) -> Optional[Dict]:
         """Parse doors and windows."""
@@ -64,6 +70,26 @@ class ConditionBasedServiceReport(VehicleDataBase):
             retval["messages"] = [ConditionBasedService.from_api_entry(**m) for m in messages]
             retval["is_service_required"] = any((m.state != ConditionBasedServiceStatus.OK) for m in retval["messages"])
 
+            retval["next_service_by_distance"] = next(
+                iter(
+                    sorted(
+                        [m for m in retval["messages"] if m.due_distance.value is not None],
+                        key=lambda x: f"{x.due_distance.value:010}-{x.service_type}",
+                    )
+                ),
+                None,
+            )
+
+            retval["next_service_by_time"] = next(
+                iter(
+                    sorted(
+                        [m for m in retval["messages"] if m.due_date is not None],
+                        key=lambda x: f"{x.due_date!s}-{x.service_type}",
+                    )
+                ),
+                None,
+            )
+
         return retval
 
 
@@ -108,6 +134,8 @@ class CheckControlMessageReport(VehicleDataBase):
     has_check_control_messages: bool = False
     """Indicate if check control messages are present."""
 
+    urgent_check_control_messages: Optional[str] = None
+
     @classmethod
     def _parse_vehicle_data(cls, vehicle_data: Dict) -> Optional[Dict]:
         """Parse doors and windows."""
@@ -116,6 +144,11 @@ class CheckControlMessageReport(VehicleDataBase):
         if ATTR_STATE in vehicle_data and (messages := vehicle_data[ATTR_STATE].get("checkControlMessages")):
             retval["messages"] = [CheckControlMessage.from_api_entry(**m) for m in messages if m["severity"] != "OK"]
             retval["has_check_control_messages"] = len([m for m in retval["messages"] if m.state != "LOW"]) > 0
+            retval["urgent_check_control_messages"] = (
+                ", ".join([m.description_short for m in retval["messages"] if m.state != "LOW"])
+                if retval["has_check_control_messages"]
+                else None
+            )
 
         return retval
 


Test patch:
diff --git a/bimmer_connected/tests/responses/G01/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO04.json b/bimmer_connected/tests/responses/G01/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO04.json
index 88cd7c5..3e7a958 100644
--- a/bimmer_connected/tests/responses/G01/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO04.json
+++ b/bimmer_connected/tests/responses/G01/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO04.json
@@ -251,14 +251,14 @@
                 "type": "BRAKE_FLUID"
             },
             {
-                "dateTime": "2024-12-01T00:00:00.000Z",
+                "dateTime": "2024-11-01T00:00:00.000Z",
                 "description": "",
                 "mileage": 50000,
                 "status": "OK",
                 "type": "VEHICLE_TUV"
             },
             {
-                "dateTime": "2024-12-01T00:00:00.000Z",
+                "dateTime": "2024-11-01T00:00:00.000Z",
                 "description": "",
                 "mileage": 50000,
                 "status": "OK",
diff --git a/bimmer_connected/tests/test_vehicle_status.py b/bimmer_connected/tests/test_vehicle_status.py
index 9a869a2..1ab3164 100644
--- a/bimmer_connected/tests/test_vehicle_status.py
+++ b/bimmer_connected/tests/test_vehicle_status.py
@@ -210,10 +210,16 @@ async def test_plugged_in_waiting_for_charge_window(caplog, bmw_fixture: respx.R
 @pytest.mark.asyncio
 async def test_condition_based_services(caplog, bmw_fixture: respx.Router):
     """Test condition based service messages."""
-    vehicle = (await prepare_account_with_vehicles()).get_vehicle(VIN_G26)
+    vehicle = (await prepare_account_with_vehicles()).get_vehicle(VIN_G01)
+
+    assert vehicle.condition_based_services.next_service_by_distance.due_distance == (50000, "km")
+    assert vehicle.condition_based_services.next_service_by_distance.service_type == "BRAKE_FLUID"
+
+    assert vehicle.condition_based_services.next_service_by_time.due_date == datetime.datetime(2024, 11, 1, tzinfo=UTC)
+    assert vehicle.condition_based_services.next_service_by_time.service_type == "VEHICLE_CHECK"
 
     cbs = vehicle.condition_based_services.messages
-    assert len(cbs) == 5
+    assert len(cbs) == 4
     assert cbs[0].state == ConditionBasedServiceStatus.OK
     expected_cbs0 = datetime.datetime(year=2024, month=12, day=1, tzinfo=UTC)
     assert expected_cbs0 == cbs[0].due_date
@@ -225,7 +231,7 @@ async def test_condition_based_services(caplog, bmw_fixture: respx.Router):
     assert cbs[1].due_distance == (50000, "km")
 
     assert cbs[2].state == ConditionBasedServiceStatus.OK
-    expected_cbs2 = datetime.datetime(year=2024, month=12, day=1, tzinfo=UTC)
+    expected_cbs2 = datetime.datetime(year=2024, month=11, day=1, tzinfo=UTC)
     assert expected_cbs2 == cbs[2].due_date
     assert cbs[2].due_distance == (50000, "km")
 
@@ -367,6 +373,7 @@ async def test_check_control_messages(caplog, bmw_fixture: respx.Router):
     """
     vehicle = (await prepare_account_with_vehicles()).get_vehicle(VIN_G01)
     assert vehicle.check_control_messages.has_check_control_messages is True
+    assert vehicle.check_control_messages.urgent_check_control_messages == "ENGINE_OIL"
 
     ccms = vehicle.check_control_messages.messages
     assert len(ccms) == 2
@@ -377,6 +384,7 @@ async def test_check_control_messages(caplog, bmw_fixture: respx.Router):
 
     vehicle = (await prepare_account_with_vehicles()).get_vehicle(VIN_G20)
     assert vehicle.check_control_messages.has_check_control_messages is False
+    assert vehicle.check_control_messages.urgent_check_control_messages is None
 
     ccms = vehicle.check_control_messages.messages
     assert len(ccms) == 2