Repository: bimmerconnected/bimmer_connected
Pull request #213 (2020-10-24 16:53:11)

Select available state services from vehicles list
## 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.
-->
This PR adds code that checks for available `state` services from the base vehicles list. If one's car is to old to support e.g. `statistics`, calling the endpoint will return an empty list. However the additional network traffic/waiting time is unecessary.

Also, cars `conventional` engines seem not to be able to call the `efficiency` and `navigation` endpoints. In case of a conventional car, they even return `403` errors, rising exceptions.
__Please test this with your cars thoroughly, I don't know if another selection would be fitting better!__

@gerard33, @Markinus, @netdata-be, could you please verify this is working as expected?

## 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)
- [X] 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.


Markinus: Hi,
I like the Idea but we could do it in a different way, a suggestion only:
- I would suggest to check against != "NOT_SUPPORTED"
- We could add a validate attribute for all services, except vehicle, and check the availability of the services directly in the update procedure. Like:
state.py in __init__
       self._check_atribute = {
            SERVICE_STATUS: 'model', -> only a dummy because always available
            SERVICE_LAST_TRIP: 'statisticsAvailable',
            SERVICE_ALL_TRIPS: 'statisticsAvailable',
            SERVICE_CHARGING_PROFILE: 'chargingControl',
            SERVICE_DESTINATIONS: 'lastDestinations',
            SERVICE_RANGEMAP: 'rangeMap',
            SERVICE_EFFICIENCY: 'chargingControl', -> NOT_SUPPORTED if not an EV or PHEV
            SERVICE_NAVIGATION: 'chargingControl'} -> NOT_SUPPORTED if not an EV or PHEV

As next exchange the for loop in update_data for check of these attributes. We would reduce the network data and the arrays would be always empty if a service is not supported.

What do you think?
Bye

gerard33: Here is my fingerprint done from master (incl. #219).

[ALL_TRIPS_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468111/ALL_TRIPS_0.txt)
[CHARGING_PROFILE_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468112/CHARGING_PROFILE_0.txt)
[DESTINATIONS_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468113/DESTINATIONS_0.txt)
[LAST_TRIP_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468114/LAST_TRIP_0.txt)
[STATUS_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468115/STATUS_0.txt)
[vehicles_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468116/vehicles_0.txt)

gerard33: And here is the fingerprint with this PR #213.

[STATUS_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468119/STATUS_0.txt)
[vehicles_0.txt](https://github.com/bimmerconnected/bimmer_connected/files/5468120/vehicles_0.txt)


Patch:
diff --git a/bimmer_connected/all_trips.py b/bimmer_connected/all_trips.py
index 1b6f114..10fded9 100644
--- a/bimmer_connected/all_trips.py
+++ b/bimmer_connected/all_trips.py
@@ -27,50 +27,50 @@ class StatisticValues:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, stats_dict: dict):
+        self._stats_dict = stats_dict
 
     @property
     @backend_parameter_statistic
     def community_low(self) -> float:
         """Get the community low value."""
-        return float(self._ccm_dict["communityLow"])
+        return float(self._stats_dict["communityLow"])
 
     @property
     @backend_parameter_statistic
     def community_average(self) -> float:
         """Get the community average value."""
-        return float(self._ccm_dict["communityAverage"])
+        return float(self._stats_dict["communityAverage"])
 
     @property
     @backend_parameter_statistic
     def community_high(self) -> float:
         """Get the community high value."""
-        return float(self._ccm_dict["communityHigh"])
+        return float(self._stats_dict["communityHigh"])
 
     @property
     @backend_parameter_statistic
     def user_average(self) -> float:
         """Get the user average value."""
-        return float(self._ccm_dict["userAverage"])
+        return float(self._stats_dict["userAverage"])
 
     @property
     @backend_parameter_statistic
     def user_high(self) -> float:
         """Get the user high value."""
-        return float(self._ccm_dict["userHigh"])
+        return float(self._stats_dict["userHigh"])
 
     @property
     @backend_parameter_statistic
     def user_total(self) -> float:
         """Get the user total value."""
-        return float(self._ccm_dict["userTotal"])
+        return float(self._stats_dict["userTotal"])
 
     @property
     @backend_parameter_statistic
     def user_current_charge_cycle(self) -> float:
         """Get the users current charge cycle."""
-        return float(self._ccm_dict["userCurrentChargeCycle"])
+        return float(self._stats_dict["userCurrentChargeCycle"])
 
 
 def backend_parameter(func):
diff --git a/bimmer_connected/charging_profile.py b/bimmer_connected/charging_profile.py
index 70562f6..e3dcbaf 100644
--- a/bimmer_connected/charging_profile.py
+++ b/bimmer_connected/charging_profile.py
@@ -34,18 +34,18 @@ class ChargingWindow:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, window_dict: dict):
+        self._window_dict = window_dict
 
     @property
     def start_time(self) -> str:
         """Start of the charging window."""
-        return self._ccm_dict["startTime"]
+        return self._window_dict["startTime"]
 
     @property
     def end_time(self) -> str:
         """End of the charging window."""
-        return self._ccm_dict["endTime"]
+        return self._window_dict["endTime"]
 
 
 class ClimatizationTimer:
@@ -53,23 +53,23 @@ class ClimatizationTimer:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, clima_dict: dict):
+        self._clima_dict = clima_dict
 
     @property
     def departure_time(self) -> str:
         """Deperture time for this timer."""
-        return self._ccm_dict["departureTime"]
+        return self._clima_dict["departureTime"]
 
     @property
     def timer_enabled(self) -> bool:
         """Is the timer enabled."""
-        return self._ccm_dict["timerEnabled"]
+        return self._clima_dict["timerEnabled"]
 
     @property
     def weekdays(self) -> List[str]:
         """Active weekdays for this timer."""
-        return self._ccm_dict["weekdays"]
+        return self._clima_dict["weekdays"]
 
 
 def backend_parameter(func):
diff --git a/bimmer_connected/cli.py b/bimmer_connected/cli.py
index 5d5a90a..1140206 100644
--- a/bimmer_connected/cli.py
+++ b/bimmer_connected/cli.py
@@ -104,7 +104,7 @@ def get_status(args) -> None:
         print('Vehicle properties:')
         print(json.dumps(vehicle.attributes, indent=4))
         print('Vehicle status:')
-        print(json.dumps(vehicle.state.attributes, indent=4))
+        print(json.dumps(vehicle.state.vehicle_status.attributes, indent=4))
 
 
 def fingerprint(args) -> None:
diff --git a/bimmer_connected/efficiency.py b/bimmer_connected/efficiency.py
index 075b6d1..ec34128 100644
--- a/bimmer_connected/efficiency.py
+++ b/bimmer_connected/efficiency.py
@@ -14,23 +14,23 @@ class LastTrip:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, trip_dict: dict):
+        self._trip_dict = trip_dict
 
     @property
     def name(self) -> str:
         """Name."""
-        return self._ccm_dict["name"]
+        return self._trip_dict["name"]
 
     @property
     def unit(self) -> str:
         """Unit."""
-        return self._ccm_dict["unit"]
+        return self._trip_dict["unit"]
 
     @property
     def last_trip(self) -> str:
         """Last Trip."""
-        return self._ccm_dict["lastTrip"]
+        return self._trip_dict["lastTrip"]
 
 
 def backend_parameter_life_time(func):
@@ -54,26 +54,26 @@ class LifeTime:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, time_dict: dict):
+        self._time_dict = time_dict
 
     @property
     @backend_parameter_life_time
     def name(self) -> str:
         """Name."""
-        return self._ccm_dict["name"]
+        return self._time_dict["name"]
 
     @property
     @backend_parameter_life_time
     def unit(self) -> str:
         """Unit."""
-        return self._ccm_dict["unit"]
+        return self._time_dict["unit"]
 
     @property
     @backend_parameter_life_time
     def life_time(self) -> str:
         """Life Time."""
-        return self._ccm_dict["lifeTime"]
+        return self._time_dict["lifeTime"]
 
 
 class Characteristic:
@@ -82,18 +82,18 @@ class Characteristic:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, char_dict: dict):
+        self._char_dict = char_dict
 
     @property
     def characteristic(self) -> str:
         """Characteristic."""
-        return self._ccm_dict["characteristic"]
+        return self._char_dict["characteristic"]
 
     @property
     def quantity(self) -> int:
         """Quantity."""
-        return int(self._ccm_dict["quantity"])
+        return int(self._char_dict["quantity"])
 
 
 def backend_parameter(func):
diff --git a/bimmer_connected/last_destinations.py b/bimmer_connected/last_destinations.py
index a9cdd4e..b689d93 100644
--- a/bimmer_connected/last_destinations.py
+++ b/bimmer_connected/last_destinations.py
@@ -19,43 +19,43 @@ class Destination:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, dest_dict: dict):
+        self._dest_dict = dest_dict
 
     @property
     def latitude(self) -> float:
         """latitude of this destination."""
-        return float(self._ccm_dict["lat"])
+        return float(self._dest_dict["lat"])
 
     @property
     def longitude(self) -> float:
         """longitude of this destination."""
-        return float(self._ccm_dict["lon"])
+        return float(self._dest_dict["lon"])
 
     @property
     def country(self) -> str:
         """Country of this destination."""
-        return self._ccm_dict["country"]
+        return self._dest_dict["country"]
 
     @property
     def city(self) -> str:
         """City of this destination."""
-        return self._ccm_dict["city"]
+        return self._dest_dict["city"]
 
     @property
     def street(self) -> str:
         """Street of this destination."""
-        return self._ccm_dict["street"]
+        return self._dest_dict["street"]
 
     @property
     def destination_type(self) -> DestinationType:
         """Type of this destination."""
-        return DestinationType(self._ccm_dict["type"])
+        return DestinationType(self._dest_dict["type"])
 
     @property
     def created_at(self) -> str:
         """Date of creation of this destination."""
-        return self._ccm_dict["createdAt"]
+        return self._dest_dict["createdAt"]
 
 
 def backend_parameter(func):
diff --git a/bimmer_connected/range_maps.py b/bimmer_connected/range_maps.py
index c721113..5366582 100644
--- a/bimmer_connected/range_maps.py
+++ b/bimmer_connected/range_maps.py
@@ -9,6 +9,12 @@ from bimmer_connected.const import SERVICE_RANGEMAP
 _LOGGER = logging.getLogger(__name__)
 
 
+class RangeMapServices(Enum):
+    """Range map services."""
+    POLYGON = 'RANGE_POLYGON'
+    CIRCLE = 'RANGE_CIRCLE'
+
+
 class RangeMapType(Enum):
     """Range map types."""
     ECO_PRO_PLUS = 'ECO_PRO_PLUS'
@@ -25,18 +31,18 @@ class MapPoint:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, mp_dict: dict):
+        self._mp_dict = mp_dict
 
     @property
     def latitude(self) -> float:
         """latitude of this point."""
-        return float(self._ccm_dict["lat"])
+        return float(self._mp_dict["lat"])
 
     @property
     def longitude(self) -> float:
         """longitude of this point."""
-        return float(self._ccm_dict["lon"])
+        return float(self._mp_dict["lon"])
 
 
 class RangeMap:
@@ -44,19 +50,19 @@ class RangeMap:
     This class provides a nicer API than parsing the JSON format directly.
     """
 
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
+    def __init__(self, range_dict: dict):
+        self._range_dict = range_dict
 
     @property
     def range_map_type(self) -> RangeMapType:
         """Type of the range map."""
-        return RangeMapType(self._ccm_dict["type"])
+        return RangeMapType(self._range_dict["type"])
 
     @property
     def polyline(self) -> List[MapPoint]:
         """polylines of this range map."""
         pol_list = []
-        for map_point in self._ccm_dict["polyline"]:
+        for map_point in self._range_dict["polyline"]:
             pol_list.append(MapPoint(map_point))
         return pol_list
 
diff --git a/bimmer_connected/state.py b/bimmer_connected/state.py
index 22f3965..eb3fdcf 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),
@@ -133,220 +133,264 @@ class VehicleState:
     @property
     @backend_parameter
     def timestamp(self) -> datetime.datetime:
-        """Get the timestamp when the data was recorded."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.timestamp instead")
+        """DEPRECATED: Use state.vehicle_status.timestamp instead.
+
+        Get the timestamp when the data was recorded."""
         return self.vehicle_status.timestamp
 
     @property
     @backend_parameter
     def gps_position(self) -> (float, float):
-        """Get the last known position of the vehicle.
+        """DEPRECATED: Use state.vehicle_status.gps_position instead.
+
+        Get the last known position of the vehicle.
 
         Returns a tuple of (latitude, longitude).
         This only provides data, if the vehicle tracking is enabled!
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.gps_position instead")
         return self.vehicle_status.gps_position
 
     @property
     @backend_parameter
     def is_vehicle_tracking_enabled(self) -> bool:
-        """Check if the position tracking of the vehicle is enabled.
+        """DEPRECATED: Use state.vehicle_status.is_vehicle_tracking_enabled instead.
+
+        Check if the position tracking of the vehicle is enabled.
 
         The server return "OK" if tracking is enabled and "DRIVER_DISABLED" if it is disabled in the vehicle.
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.is_vehicle_tracking_enabled instead")
         return bool(self.vehicle_status.is_vehicle_tracking_enabled)
 
     @property
     @backend_parameter
     def mileage(self) -> int:
-        """Get the mileage of the vehicle.
+        """DEPRECATED: Use state.vehicle_status.mileage instead.
+
+        Get the mileage of the vehicle.
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.mileage instead")
         return self.vehicle_status.mileage
 
     @property
     @backend_parameter
     def remaining_range_fuel(self) -> int:
-        """Get the remaining range of the vehicle on fuel.
+        """DEPRECATED: Use state.vehicle_status.remaining_range_fuel instead.
+
+        Get the remaining range of the vehicle on fuel.
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.remaining_range_fuel instead")
         return self.vehicle_status.remaining_range_fuel
 
     @property
     @backend_parameter
     def remaining_fuel(self) -> int:
-        """Get the remaining fuel of the vehicle.
+        """DEPRECATED: Use state.vehicle_status.remaining_fuel instead.
+
+        Get the remaining fuel of the vehicle.
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.remaining_fuel instead")
         return self.vehicle_status.remaining_fuel
 
     @property
     @backend_parameter
     def lids(self) -> List[Lid]:
-        """Get all lids (doors+hatch+trunk) of the car."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.lids instead")
+        """DEPRECATED: Use state.vehicle_status.lids instead.
+
+        Get all lids (doors+hatch+trunk) of the car.
+        """
         return self.vehicle_status.lids
 
     @property
     @backend_parameter
     def open_lids(self) -> List[Lid]:
-        """Get all open lids of the car."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.open_lids instead")
+        """DEPRECATED: Use state.vehicle_status.open_lids instead.
+
+        Get all open lids of the car.
+        """
         return self.vehicle_status.open_lids
 
     @property
     @backend_parameter
     def all_lids_closed(self) -> bool:
-        """Check if all lids are closed."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.all_lids_closed instead")
+        """DEPRECATED: Use state.vehicle_status.all_lids_closed instead.
+
+        Check if all lids are closed.
+        """
         return self.vehicle_status.all_lids_closed
 
     @property
     @backend_parameter
     def windows(self) -> List[Window]:
-        """Get all windows (doors+sun roof) of the car."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.windows instead")
+        """DEPRECATED: Use state.vehicle_status.windows instead.
+
+        Get all windows (doors+sun roof) of the car.
+        """
         return self.vehicle_status.windows
 
     @property
     @backend_parameter
     def open_windows(self) -> List[Window]:
-        """Get all open windows of the car."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.open_windows instead")
+        """DEPRECATED: Use state.vehicle_status.open_windows instead.
+
+        Get all open windows of the car.
+        """
         return self.vehicle_status.open_windows
 
     @property
     @backend_parameter
     def all_windows_closed(self) -> bool:
-        """Check if all windows are closed."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.all_windows_closed instead")
+        """DEPRECATED: Use state.vehicle_status.all_windows_closed instead.
+
+        Check if all windows are closed.
+        """
         return self.vehicle_status.all_windows_closed
 
     @property
     @backend_parameter
     def door_lock_state(self) -> LockState:
-        """Get state of the door locks."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.all_windows_closed instead")
+        """DEPRECATED: Use state.vehicle_status.all_windows_closed instead.
+
+        Get state of the door locks.
+        """
         return self.vehicle_status.door_lock_state
 
     @property
     @backend_parameter
     def last_update_reason(self) -> str:
-        """The reason for the last state update"""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.last_update_reason instead")
+        """DEPRECATED: Use state.vehicle_status.last_update_reason instead.
+
+        The reason for the last state update"""
         return self.vehicle_status.last_update_reason
 
     @property
     @backend_parameter
     def last_charging_end_result(self) -> str:
-        """Get the last charging end result"""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.last_charging_end_result instead")
+        """DEPRECATED: Use state.vehicle_status.last_charging_end_result instead.
+
+        Get the last charging end result"""
         return self.vehicle_status.last_charging_end_result
 
     @property
     @backend_parameter
     def connection_status(self) -> str:
-        """Get status of the connection"""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.connection_status instead")
+        """DEPRECATED: Use state.vehicle_status.connection_status instead.
+
+        Get status of the connection"""
         return self.vehicle_status.connection_status
 
     @property
     @backend_parameter
     def condition_based_services(self) -> List[ConditionBasedServiceReport]:
-        """Get status of the condition based services."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.condition_based_services instead")
+        """DEPRECATED: Use state.vehicle_status.condition_based_services instead.
+
+        Get status of the condition based services.
+        """
         return self.vehicle_status.condition_based_services
 
     @property
     @backend_parameter
     def are_all_cbs_ok(self) -> bool:
-        """Check if the status of all condition based services is "OK"."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.are_all_cbs_ok instead")
+        """DEPRECATED: Use state.vehicle_status.are_all_cbs_ok instead.
+
+        Check if the status of all condition based services is "OK".
+        """
         return bool(self.vehicle_status.are_all_cbs_ok)
 
     @property
     @backend_parameter
     def parking_lights(self) -> ParkingLightState:
-        """Get status of parking lights.
+        """DEPRECATED: Use state.vehicle_status.parking_lights instead.
+
+        Get status of parking lights.
 
         :returns None if status is unknown.
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.parking_lights instead")
         return self.vehicle_status.parking_lights
 
     @property
     @backend_parameter
     def are_parking_lights_on(self) -> bool:
-        """Get status of parking lights.
+        """DEPRECATED: Use state.vehicle_status.are_parking_lights_on instead.
+
+        Get status of parking lights.
 
         :returns None if status is unknown.
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.are_parking_lights_on instead")
         return bool(self.vehicle_status.are_parking_lights_on)
 
     @property
     @backend_parameter
     def remaining_range_electric(self) -> int:
-        """Remaining range on battery, in kilometers."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.remaining_range_electric instead")
+        """DEPRECATED: Use state.vehicle_status.remaining_range_electric instead.
+
+        Remaining range on battery, in kilometers.
+        """
         return self.vehicle_status.remaining_range_electric
 
     @property
     @backend_parameter
     def remaining_range_total(self) -> int:
-        """Get the total remaining range of the vehicle in kilometers.
+        """DEPRECATED: Use state.vehicle_status.remaining_range_total instead.
 
+        Get the total remaining range of the vehicle in kilometers.
         That is electrical range + fuel range.
         """
-        _LOGGER.warning("Function deprecated use state.vehicle_status.remaining_range_total instead")
         return self.vehicle_status.remaining_range_total
 
     @property
     @backend_parameter
     def max_range_electric(self) -> int:
-        """ This can change with driving style and temperature in kilometers."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.max_range_electric instead")
+        """DEPRECATED: Use state.vehicle_status.max_range_electric instead.
+
+        Maximum range on battery, in kilometers.
+        This can change with driving style and temperature.
+        """
         return self.vehicle_status.max_range_electric
 
     @property
     @backend_parameter
     def charging_status(self) -> ChargingState:
-        """Charging state of the vehicle."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.charging_status instead")
+        """DEPRECATED: Use state.vehicle_status.charging_status instead.
+
+        Charging state of the vehicle.
+        """
         return self.vehicle_status.charging_status
 
     @property
     @backend_parameter
     def charging_time_remaining(self) -> datetime.timedelta:
-        """Get the remaining charging time."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.charging_time_remaining instead")
+        """DEPRECATED: Use state.vehicle_status.charging_time_remaining instead.
+
+        Get the remaining charging time.
+        """
         return self.vehicle_status.charging_time_remaining
 
     @property
     @backend_parameter
     def charging_level_hv(self) -> int:
-        """State of charge of the high voltage battery in percent."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.charging_level_hv instead")
+        """DEPRECATED: Use state.vehicle_status.charging_level_hv instead.
+
+        State of charge of the high voltage battery in percent.
+        """
         return self.vehicle_status.charging_level_hv
 
     @property
     @backend_parameter
     def check_control_messages(self) -> List[CheckControlMessage]:
-        """List of check control messages."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.check_control_messages instead")
+        """DEPRECATED: Use state.vehicle_status.check_control_messages instead.
+
+        List of check control messages.
+        """
         return self.vehicle_status.check_control_messages
 
     @property
     @backend_parameter
     def has_check_control_messages(self) -> bool:
-        """Return true if any check control message is present."""
-        _LOGGER.warning("Function deprecated use state.vehicle_status.has_check_control_messages instead")
+        """DEPRECATED: Use state.vehicle_status.has_check_control_messages instead.
+
+        Return true if any check control message is present.
+        """
         return bool(self.vehicle_status.has_check_control_messages)
diff --git a/bimmer_connected/vehicle.py b/bimmer_connected/vehicle.py
index bbc99d7..a37d2d1 100644
--- a/bimmer_connected/vehicle.py
+++ b/bimmer_connected/vehicle.py
@@ -5,8 +5,11 @@ from typing import List
 
 from bimmer_connected.state import VehicleState
 from bimmer_connected.vehicle_status import WINDOWS, LIDS
+from bimmer_connected.range_maps import RangeMapServices
 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 +110,33 @@ 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') != "NOT_SUPPORTED"
+
+    @property
+    def has_destination_service(self) -> bool:
+        """Return True if destinations are available."""
+        return self.attributes.get('lastDestinations') != "NOT_SUPPORTED"
+
+    @property
+    def has_rangemap_service(self) -> bool:
+        """Return True if rangemap (range circle) is available."""
+        return self.attributes.get('rangeMap') != "NOT_SUPPORTED"
+
+    @property
+    def rangemap_service_type(self) -> RangeMapServices:
+        """Returns the vehicles rangemap service type if available."""
+        if self.has_rangemap_service:
+            return RangeMapServices[self.attributes.get('rangeMap')]
+        return None
+
     @property
     def drive_train_attributes(self) -> List[str]:
         """Get list of attributes available for the drive train of the vehicle.
@@ -156,6 +186,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))