Repository: bimmerconnected/bimmer_connected
Pull request #217 (2020-10-25 11:54:25)

Select available state services from vehicles list
PR #213 to separate branch to create beta release.



Patch:
diff --git a/bimmer_connected/state.py b/bimmer_connected/state.py
index bcd4493..75c4917 100644
--- a/bimmer_connected/state.py
+++ b/bimmer_connected/state.py
@@ -94,7 +94,7 @@ class VehicleState:
             'dlon': self._vehicle.observer_longitude,
         }
 
-        for service in self._url:
+        for service in self._vehicle.available_state_services:
             try:
                 response = self._account.send_request(
                     self._url[service].format(server=self._account.server_url, vin=self._vehicle.vin),
diff --git a/bimmer_connected/vehicle.py b/bimmer_connected/vehicle.py
index bbc99d7..0b3e258 100644
--- a/bimmer_connected/vehicle.py
+++ b/bimmer_connected/vehicle.py
@@ -6,7 +6,9 @@ from typing import List
 from bimmer_connected.state import VehicleState
 from bimmer_connected.vehicle_status import WINDOWS, LIDS
 from bimmer_connected.remote_services import RemoteServices
-from bimmer_connected.const import VEHICLE_IMAGE_URL
+from bimmer_connected.const import VEHICLE_IMAGE_URL, SERVICE_ALL_TRIPS, SERVICE_CHARGING_PROFILE, \
+    SERVICE_DESTINATIONS, SERVICE_EFFICIENCY, SERVICE_LAST_TRIP, SERVICE_NAVIGATION, \
+    SERVICE_RANGEMAP, SERVICE_STATUS
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -107,6 +109,26 @@ class ConnectedDriveVehicle:
         In this case we can get the state of the gas tank."""
         return self.drive_train in COMBUSTION_ENGINE_DRIVE_TRAINS
 
+    @property
+    def has_statistics_service(self) -> bool:
+        """Return True if statistics are available."""
+        return self.attributes.get('statisticsAvailable')
+
+    @property
+    def has_weekly_planner_service(self) -> bool:
+        """Return True if charging control (weekly planner) is available."""
+        return self.attributes.get('chargingControl') == "WEEKLY_PLANNER"
+
+    @property
+    def has_destination_service(self) -> bool:
+        """Return True if destinations are available."""
+        return self.attributes.get('lastDestinations') == "SUPPORTED"
+
+    @property
+    def has_rangemap_service(self) -> bool:
+        """Return True if rangemap (range circle) is available."""
+        return self.attributes.get('rangeMap') == "RANGE_CIRCLE"
+
     @property
     def drive_train_attributes(self) -> List[str]:
         """Get list of attributes available for the drive train of the vehicle.
@@ -156,6 +178,27 @@ class ConnectedDriveVehicle:
             result += ['lights_parking', 'lids', 'windows']
         return result
 
+    @property
+    def available_state_services(self) -> List:
+        """Get the list of all available state services for this vehicle."""
+        result = [SERVICE_STATUS]
+        if self.has_statistics_service:
+            result += [SERVICE_LAST_TRIP, SERVICE_ALL_TRIPS]
+
+        if self.has_weekly_planner_service:
+            result += [SERVICE_CHARGING_PROFILE]
+
+        if self.has_destination_service:
+            result += [SERVICE_DESTINATIONS]
+
+        if self.has_rangemap_service:
+            result += [SERVICE_RANGEMAP]
+
+        if self.drive_train != DriveTrainType.CONVENTIONAL:
+            result += [SERVICE_EFFICIENCY, SERVICE_NAVIGATION]
+
+        return result
+
     def get_vehicle_image(self, width: int, height: int, direction: VehicleViewDirection) -> bytes:
         """Get a rendered image of the vehicle.
 


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index 2b97d3d..88b555c 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -88,6 +88,13 @@ MISSING_ATTRIBUTES = [
     'vehicleCountry',             # para not available in all vehicles
 ]
 
+AVAILABLE_STATES_MAPPING = {
+    "statisticsAvailable": {True: ["LAST_TRIP", "ALL_TRIPS"]},
+    "chargingControl": {"WEEKLY_PLANNER": ["CHARGING_PROFILE"]},
+    "lastDestinations": {"SUPPORTED": ["DESTINATIONS"]},
+    "rangeMap": {"RANGE_CIRCLE": ["RANGEMAP"]}
+}
+
 POI_DATA = {
     "lat": 37.4028943,
     "lon": -121.9700289,
diff --git a/test/test_vehicle.py b/test/test_vehicle.py
index 0e78683..693d441 100644
--- a/test/test_vehicle.py
+++ b/test/test_vehicle.py
@@ -3,7 +3,7 @@ import unittest
 from unittest import mock
 from test import load_response_json, BackendMock, TEST_USERNAME, TEST_PASSWORD, TEST_REGION, \
     G31_VIN, F48_VIN, I01_VIN, I01_NOREX_VIN, F15_VIN, F45_VIN, F31_VIN, TEST_VEHICLE_DATA, \
-    ATTRIBUTE_MAPPING, MISSING_ATTRIBUTES, ADDITIONAL_ATTRIBUTES, G30_PHEV_OS7_VIN
+    ATTRIBUTE_MAPPING, MISSING_ATTRIBUTES, ADDITIONAL_ATTRIBUTES, G30_PHEV_OS7_VIN, AVAILABLE_STATES_MAPPING
 
 from bimmer_connected.vehicle import ConnectedDriveVehicle, DriveTrainType
 from bimmer_connected.account import ConnectedDriveAccount
@@ -34,6 +34,10 @@ class TestVehicle(unittest.TestCase):
             self.assertIsNotNone(vehicle.has_internal_combustion_engine)
             self.assertIsNotNone(vehicle.has_hv_battery)
             self.assertIsNotNone(vehicle.drive_train_attributes)
+            self.assertIsNotNone(vehicle.has_statistics_service)
+            self.assertIsNotNone(vehicle.has_weekly_planner_service)
+            self.assertIsNotNone(vehicle.has_destination_service)
+            self.assertIsNotNone(vehicle.has_rangemap_service)
 
     def test_drive_train_attributes(self):
         """Test parsing different attributes of the vehicle."""
@@ -73,3 +77,30 @@ class TestVehicle(unittest.TestCase):
                                           if a not in MISSING_ATTRIBUTES])
             expected_attributes = sorted([a for a in vehicle.available_attributes if a not in ADDITIONAL_ATTRIBUTES])
             self.assertListEqual(existing_attributes, expected_attributes)
+
+    def test_available_state_services(self):
+        """Check that available_attributes returns exactly the arguments we have in our test data."""
+        backend_mock = BackendMock()
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+
+        vehicles = load_response_json('vehicles.json')
+
+        for test_vehicle in vehicles['vehicles']:
+            vehicle = account.get_vehicle(test_vehicle['vin'])
+            print(vehicle.name)
+
+            services_to_check = {
+                k: v
+                for k, v in test_vehicle.items()
+                if k in list(AVAILABLE_STATES_MAPPING)
+            }
+
+            available_services = ['STATUS']
+            for key, value in services_to_check.items():
+                if AVAILABLE_STATES_MAPPING[key].get(value):
+                    available_services += AVAILABLE_STATES_MAPPING[key][value]
+            if vehicle.drive_train != DriveTrainType.CONVENTIONAL:
+                available_services += ['EFFICIENCY', 'NAVIGATION']
+
+            self.assertListEqual(sorted(vehicle.available_state_services), sorted(available_services))