Repository: bimmerconnected/bimmer_connected
Pull request #191 (2020-07-28 20:53:44)

Added new classes for statistic, destinations, rangemaps and trip services
This is a different suggestion implementation of the sub services to https://github.com/bimmerconnected/bimmer_connected/pull/137.
The data updating is in one place and all services are handled in own classes and files. The user is able to read all values directly with methods. In this first step the subclasses and implementations only.
What do you think about it?


Markinus: @rikroe,  in the first step the code ist done. It would be great if you could review it whether this extension is ok for you or not. If yes then I would add some tests and finish it.

rikroe: Nicely done, many thanks! Thats a lot of work!

Cannot promise you to fully review your changes within the next week, unfortunately. But looking forward to it!
Markinus: The next 2 weeks I'm not available too, so it's not a problem.
Markinus: Hi @rikroe,
could you check this PR?
rikroe: This looks very good! Unfortunately I cannot test most of the functions (my car is too old)...
Therefore I am really dependent on tests and can only approve your changes with a peaceful mind if there are tests for these as well (so one dummy car that has all the settings etc.).
With that done, I am happy to approve this!

And one remark: It's `deprecated` instead of `depreceted` in `state.py` ;) Maybe we could also throw warnings instead of info.
But really like that you keep it backwards-compatible!

Many thanks for your work!
Rantaki: Really looking forward getting the statistics of the trips! Does this PR cover the possibility to start immediate charging?

No idea what is needed to get this work through Home Assistant but hopefully it can be done. I was looking for a way to start the charging but I've been also wondering why original BMW app does not provide the trip statistics.
Markinus: Hi @rikroe,
ok, I will finish this PR and extend the tests.
Markinus: Hi @rikroe,

I just added some tests. It would be great if you could check it. What do you think?

rikroe: Very nice work! I think it is almost ready to be merged!
The _coverall_ check is now failing because quite some parts of `state.py` are not tested - especially the ones that throw a `deprecation` warning:
https://coveralls.io/builds/33587743/source?filename=bimmer_connected%2Fstate.py

I think it would be great to have the old `state` calls/attributes in there as well, throwing a warning but actually returning the correct value (so it is as well checked that we're compatible to previous versions).

Also, it might be a good test to use a vehicle that doesn't have all the information so we don't get any unexpected behavior (now or some time in the future).

Many thanks!
Markinus: @rikroe, I restored the tests for the depreced functions. Now all tests have passed successfuly.
I tested it already with different data (from my G30 PHEV where some stuff is missing). I will add the G30_PHEV data in the future to this repo but I would suggest to do it maybe in a second step. Ok for you?
Markinus: @rikroe, done. Now are all tests "green"
Markinus: @rikroe, I've prepared the testdata of the G30 and two further lagacy services (e.g. for soc info). I will create a new PR if this is merged.

Patch:
diff --git a/bimmer_connected/all_trips.py b/bimmer_connected/all_trips.py
new file mode 100644
index 0000000..d30ac1a
--- /dev/null
+++ b/bimmer_connected/all_trips.py
@@ -0,0 +1,162 @@
+"""Models the all trips of a vehicle."""
+
+import logging
+
+from bimmer_connected.const import SERVICE_ALL_TRIPS
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def backend_parameter_statistic(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'StatisticValues', *args, **kwargs):
+        # pylint: disable=protected-access
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+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
+
+    @property
+    @backend_parameter_statistic
+    def community_low(self) -> float:
+        """Get the community low value."""
+        return float(self._ccm_dict["communityLow"])
+
+    @property
+    @backend_parameter_statistic
+    def community_average(self) -> float:
+        """Get the community average value."""
+        return float(self._ccm_dict["communityAverage"])
+
+    @property
+    @backend_parameter_statistic
+    def community_high(self) -> float:
+        """Get the community high value."""
+        return float(self._ccm_dict["communityHigh"])
+
+    @property
+    @backend_parameter_statistic
+    def user_average(self) -> float:
+        """Get the user average value."""
+        return float(self._ccm_dict["userAverage"])
+
+    @property
+    @backend_parameter_statistic
+    def user_high(self) -> float:
+        """Get the user high value."""
+        return float(self._ccm_dict["userHigh"])
+
+    @property
+    @backend_parameter_statistic
+    def user_total(self) -> float:
+        """Get the user total value."""
+        return float(self._ccm_dict["userTotal"])
+
+    @property
+    @backend_parameter_statistic
+    def user_current_charge_cycle(self) -> float:
+        """Get the users current change cycle."""
+        return float(self._ccm_dict["userCurrentChargeCycle"])
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'AllTrips', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_ALL_TRIPS] is None:
+            raise ValueError('No data available for vehicles trips!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class AllTrips:  # pylint: disable=too-many-public-methods
+    """Models the all trips service of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_ALL_TRIPS][item]
+
+    @property
+    @backend_parameter
+    def reset_date(self) -> str:
+        """Returns the average combined consumption."""
+        return self._state.attributes[SERVICE_ALL_TRIPS]['resetDate']
+
+    @property
+    @backend_parameter
+    def battery_size_max(self) -> int:
+        """Maximal battery size, in Wh."""
+        return int(self._state.attributes[SERVICE_ALL_TRIPS]['batterySizeMax'])
+
+    @property
+    @backend_parameter
+    def saved_co2(self) -> int:
+        """Maximal battery size, in Wh."""
+        return float(self._state.attributes[SERVICE_ALL_TRIPS]['savedCO2'])
+
+    @property
+    @backend_parameter
+    def saved_co2_green_energy(self) -> int:
+        """Maximal battery size, in Wh."""
+        return float(self._state.attributes[SERVICE_ALL_TRIPS]['savedCO2greenEnergy'])
+
+    @property
+    @backend_parameter
+    def total_saved_fuel(self) -> int:
+        """Maximal battery size, in Wh."""
+        return float(self._state.attributes[SERVICE_ALL_TRIPS]['totalSavedFuel'])
+
+    @property
+    @backend_parameter
+    def average_electric_consumption(self) -> StatisticValues:
+        """Returns the average electric consumption."""
+        return StatisticValues(self._state.attributes[SERVICE_ALL_TRIPS]['avgElectricConsumption'])
+
+    @property
+    @backend_parameter
+    def average_recopuration(self) -> StatisticValues:
+        """Returns the average recopuration."""
+        return StatisticValues(self._state.attributes[SERVICE_ALL_TRIPS]['avgRecuperation'])
+
+    @property
+    @backend_parameter
+    def chargecycle_range(self) -> StatisticValues:
+        """Returns the charge cycle range."""
+        return StatisticValues(self._state.attributes[SERVICE_ALL_TRIPS]['chargecycleRange'])
+
+    @property
+    @backend_parameter
+    def total_electric_distance(self) -> StatisticValues:
+        """Returns the total electric distance."""
+        return StatisticValues(self._state.attributes[SERVICE_ALL_TRIPS]['totalElectricDistance'])
+
+    @property
+    @backend_parameter
+    def average_combined_consumption(self) -> StatisticValues:
+        """Returns the average combined consumption."""
+        return StatisticValues(self._state.attributes[SERVICE_ALL_TRIPS]['avgCombinedConsumption'])
diff --git a/bimmer_connected/charging_profile.py b/bimmer_connected/charging_profile.py
new file mode 100644
index 0000000..70562f6
--- /dev/null
+++ b/bimmer_connected/charging_profile.py
@@ -0,0 +1,146 @@
+"""Models the charging profiles of a vehicle."""
+
+import logging
+from typing import List
+from enum import Enum
+
+from bimmer_connected.const import SERVICE_CHARGING_PROFILE
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ChargingMode(Enum):
+    """Charging mode of electric vehicle."""
+    IMMEDIATE_CHARGING = 'IMMEDIATE_CHARGING'
+    DELAYED_CHARGING = 'DELAYED_CHARGING'
+
+
+class ChargingPreferences(Enum):
+    """Charging preferences of electric vehicle."""
+    NO_PRESELECTION = 'NO_PRESELECTION'
+    CHARGING_WINDOW = 'CHARGING_WINDOW'
+
+
+class TimerTypes(Enum):
+    """Different Timer-Types."""
+    TIMER_1 = 'timer1'
+    TIMER_2 = 'timer2'
+    TIMER_3 = 'timer3'
+    OVERRIDE_TIMER = 'overrideTimer'
+
+
+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
+
+    @property
+    def start_time(self) -> str:
+        """Start of the charging window."""
+        return self._ccm_dict["startTime"]
+
+    @property
+    def end_time(self) -> str:
+        """End of the charging window."""
+        return self._ccm_dict["endTime"]
+
+
+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
+
+    @property
+    def departure_time(self) -> str:
+        """Deperture time for this timer."""
+        return self._ccm_dict["departureTime"]
+
+    @property
+    def timer_enabled(self) -> bool:
+        """Is the timer enabled."""
+        return self._ccm_dict["timerEnabled"]
+
+    @property
+    def weekdays(self) -> List[str]:
+        """Active weekdays for this timer."""
+        return self._ccm_dict["weekdays"]
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'ChargingProfile', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_CHARGING_PROFILE] is None:
+            raise ValueError('No data available for vehicles charging profile!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class ChargingProfile:  # pylint: disable=too-many-public-methods
+    """Models the charging profile of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    @property
+    @backend_parameter
+    def attributes(self) -> dict:
+        """Retrieve all attributes from the sever.
+
+        This does not parse the results in any way.
+        """
+        return self._state.attributes[SERVICE_CHARGING_PROFILE]
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_CHARGING_PROFILE][item]
+
+    @property
+    @backend_parameter
+    def is_pre_entry_climatization_enabled(self) -> bool:
+        """Get status of pre-entry climatization."""
+        return bool(self._state.attributes[SERVICE_CHARGING_PROFILE]['climatizationEnabled'])
+
+    @property
+    @backend_parameter
+    def pre_entry_climatization_timer(self) -> dict:
+        """List of pre entry climatization timer messages."""
+        timer_list = {}
+        for timer in TimerTypes:
+            try:
+                timer_list[timer] = ClimatizationTimer(self._state.attributes[SERVICE_CHARGING_PROFILE][timer.value])
+            except KeyError:
+                _LOGGER.debug('Timer %s not found', timer.value)
+        return timer_list
+
+    @property
+    @backend_parameter
+    def prefered_charging_window(self) -> ChargingWindow:
+        """Returns the prefered charging window."""
+        return ChargingWindow(self._state.attributes[SERVICE_CHARGING_PROFILE]['preferredChargingWindow'])
+
+    @property
+    @backend_parameter
+    def charging_preferences(self) -> str:
+        """Returns the prefered charging preferences."""
+        return ChargingPreferences(self._state.attributes[SERVICE_CHARGING_PROFILE]['chargingPreferences'])
+
+    @property
+    @backend_parameter
+    def charging_mode(self) -> str:
+        """Returns the prefered charging mode."""
+        return ChargingMode(self._state.attributes[SERVICE_CHARGING_PROFILE]['chargingMode'])
diff --git a/bimmer_connected/cli.py b/bimmer_connected/cli.py
index 202feba..5d5a90a 100644
--- a/bimmer_connected/cli.py
+++ b/bimmer_connected/cli.py
@@ -100,7 +100,7 @@ def get_status(args) -> None:
 
     for vehicle in account.vehicles:
         print('VIN: {}'.format(vehicle.vin))
-        print('Mileage: {}'.format(vehicle.state.mileage))
+        print('Mileage: {}'.format(vehicle.state.vehicle_status.mileage))
         print('Vehicle properties:')
         print(json.dumps(vehicle.attributes, indent=4))
         print('Vehicle status:')
diff --git a/bimmer_connected/const.py b/bimmer_connected/const.py
index 4dcf4cf..3f77caf 100644
--- a/bimmer_connected/const.py
+++ b/bimmer_connected/const.py
@@ -11,6 +11,20 @@ REMOTE_SERVICE_STATUS_URL = VEHICLE_VIN_URL + '/serviceExecutionStatus?serviceTy
 REMOTE_SERVICE_URL = VEHICLE_VIN_URL + "/executeService"
 VEHICLE_IMAGE_URL = VEHICLE_VIN_URL + "/image?width={width}&height={height}&view={view}"
 VEHICLE_POI_URL = VEHICLE_VIN_URL + '/sendpoi'
+VEHICLE_STATISTICS_URL = VEHICLE_VIN_URL + '/statistics'
+VEHICLE_STATISTICS_LAST_TRIP_URL = VEHICLE_STATISTICS_URL + '/lastTrip'
+VEHICLE_STATISTICS_ALL_TRIPS_URL = VEHICLE_STATISTICS_URL + '/allTrips'
+VEHICLE_CHARGING_PROFILE_URL = VEHICLE_VIN_URL + '/chargingprofile'
+VEHICLE_DESTINATIONS_URL = VEHICLE_VIN_URL + '/destinations'
+VEHICLE_RANGEMAP_URL = VEHICLE_VIN_URL + '/rangemap'
+
+SERVICE_STATUS = 'STATUS'
+SERVICE_LAST_TRIP = 'LAST_TRIP'
+SERVICE_ALL_TRIPS = 'ALL_TRIPS'
+SERVICE_CHARGING_PROFILE = 'CHARGING_PROFILE'
+SERVICE_DESTINATIONS = 'DESTINATIONS'
+SERVICE_RANGEMAP = 'RANGEMAP'
+
 
 # Possible error codes, other codes are mapped to UNKNOWN_ERROR
 ERROR_CODE_MAPPING = {
diff --git a/bimmer_connected/last_destinations.py b/bimmer_connected/last_destinations.py
new file mode 100644
index 0000000..a9cdd4e
--- /dev/null
+++ b/bimmer_connected/last_destinations.py
@@ -0,0 +1,105 @@
+"""Models the last destinations of a vehicle."""
+
+import logging
+from typing import List
+from enum import Enum
+
+from bimmer_connected.const import SERVICE_DESTINATIONS
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DestinationType(Enum):
+    """Types of destinations."""
+    DESTINATION = 'DESTINATION'
+
+
+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
+
+    @property
+    def latitude(self) -> float:
+        """latitude of this destination."""
+        return float(self._ccm_dict["lat"])
+
+    @property
+    def longitude(self) -> float:
+        """longitude of this destination."""
+        return float(self._ccm_dict["lon"])
+
+    @property
+    def country(self) -> str:
+        """Country of this destination."""
+        return self._ccm_dict["country"]
+
+    @property
+    def city(self) -> str:
+        """City of this destination."""
+        return self._ccm_dict["city"]
+
+    @property
+    def street(self) -> str:
+        """Street of this destination."""
+        return self._ccm_dict["street"]
+
+    @property
+    def destination_type(self) -> DestinationType:
+        """Type of this destination."""
+        return DestinationType(self._ccm_dict["type"])
+
+    @property
+    def created_at(self) -> str:
+        """Date of creation of this destination."""
+        return self._ccm_dict["createdAt"]
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'LastDestinations', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_DESTINATIONS] is None:
+            raise ValueError('No data available for vehicle destinations!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class LastDestinations:  # pylint: disable=too-many-public-methods
+    """Models the last destinations of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    @property
+    @backend_parameter
+    def attributes(self) -> dict:
+        """Retrieve all attributes from the sever.
+
+        This does not parse the results in any way.
+        """
+        return self._state.attributes[SERVICE_DESTINATIONS]
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_DESTINATIONS][item]
+
+    @property
+    @backend_parameter
+    def last_destinations(self) -> List[Destination]:
+        """Get the list of last destinations."""
+        destinations_list = []
+        for dest in self._state.attributes[SERVICE_DESTINATIONS]:
+            destinations_list.append(Destination(dest))
+        return destinations_list
diff --git a/bimmer_connected/last_trip.py b/bimmer_connected/last_trip.py
new file mode 100644
index 0000000..98c4467
--- /dev/null
+++ b/bimmer_connected/last_trip.py
@@ -0,0 +1,135 @@
+"""Models the charging profiles of a vehicle."""
+
+import logging
+
+from bimmer_connected.const import SERVICE_LAST_TRIP
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'LastTrip', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_LAST_TRIP] is None:
+            raise ValueError('No data available for vehicle last trips!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class LastTrip:  # pylint: disable=too-many-public-methods
+    """Models the last trip of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    @property
+    @backend_parameter
+    def attributes(self) -> dict:
+        """Retrieve all attributes from the sever.
+
+        This does not parse the results in any way.
+        """
+        return self._state.attributes[SERVICE_LAST_TRIP]
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_LAST_TRIP][item]
+
+    @property
+    @backend_parameter
+    def efficiency_value(self) -> float:
+        """Returns the effenciency value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['efficiencyValue'])
+
+    @property
+    @backend_parameter
+    def total_distance(self) -> float:
+        """Returns the total distance."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['totalDistance'])
+
+    @property
+    @backend_parameter
+    def electric_distance(self) -> float:
+        """Returns the electric distance."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['electricDistance'])
+
+    @property
+    @backend_parameter
+    def averege_electric_consumption(self) -> float:
+        """Returns the average electric consumption."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['avgElectricConsumption'])
+
+    @property
+    @backend_parameter
+    def averege_recuperation(self) -> float:
+        """Returns the average recuperation."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['avgRecuperation'])
+
+    @property
+    @backend_parameter
+    def driving_mode_value(self) -> float:
+        """Returns the driving mode value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['drivingModeValue'])
+
+    @property
+    @backend_parameter
+    def acceleration_value(self) -> float:
+        """Returns the acceleration value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['accelerationValue'])
+
+    @property
+    @backend_parameter
+    def anticipation_value(self) -> float:
+        """Returns the anticipation value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['anticipationValue'])
+
+    @property
+    @backend_parameter
+    def total_consumption_value(self) -> float:
+        """Returns the total consumption value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['totalConsumptionValue'])
+
+    @property
+    @backend_parameter
+    def auxiliary_consumption_value(self) -> float:
+        """Returns the auxiliary consumption value."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['auxiliaryConsumptionValue'])
+
+    @property
+    @backend_parameter
+    def averege_combined_consumption(self) -> float:
+        """Returns the averege combined consumption."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['avgCombinedConsumption'])
+
+    @property
+    @backend_parameter
+    def electric_distance_ratio(self) -> float:
+        """Returns the electric distance ratio."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['electricDistanceRatio'])
+
+    @property
+    @backend_parameter
+    def saved_fuel(self) -> float:
+        """Returns the saved fuel."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['savedFuel'])
+
+    @property
+    @backend_parameter
+    def date(self) -> str:
+        """Returns the date."""
+        return self._state.attributes[SERVICE_LAST_TRIP]['date']
+
+    @property
+    @backend_parameter
+    def duration(self) -> float:
+        """Returns the duration."""
+        return float(self._state.attributes[SERVICE_LAST_TRIP]['duration'])
diff --git a/bimmer_connected/range_maps.py b/bimmer_connected/range_maps.py
new file mode 100644
index 0000000..c721113
--- /dev/null
+++ b/bimmer_connected/range_maps.py
@@ -0,0 +1,120 @@
+"""Models the range maps of a vehicle."""
+
+import logging
+from typing import List
+from enum import Enum
+
+from bimmer_connected.const import SERVICE_RANGEMAP
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RangeMapType(Enum):
+    """Range map types."""
+    ECO_PRO_PLUS = 'ECO_PRO_PLUS'
+    COMFORT = 'COMFORT'
+
+
+class RangeMapQuality(Enum):
+    """Range map types."""
+    AVERAGE = 'AVERAGE'
+
+
+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
+
+    @property
+    def latitude(self) -> float:
+        """latitude of this point."""
+        return float(self._ccm_dict["lat"])
+
+    @property
+    def longitude(self) -> float:
+        """longitude of this point."""
+        return float(self._ccm_dict["lon"])
+
+
+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
+
+    @property
+    def range_map_type(self) -> RangeMapType:
+        """Type of the range map."""
+        return RangeMapType(self._ccm_dict["type"])
+
+    @property
+    def polyline(self) -> List[MapPoint]:
+        """polylines of this range map."""
+        pol_list = []
+        for map_point in self._ccm_dict["polyline"]:
+            pol_list.append(MapPoint(map_point))
+        return pol_list
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'RangeMaps', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_RANGEMAP] is None:
+            raise ValueError('No data available for range maps!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class RangeMaps:  # pylint: disable=too-many-public-methods
+    """Models the range maps of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    @property
+    @backend_parameter
+    def attributes(self) -> dict:
+        """Retrieve all attributes from the sever.
+
+        This does not parse the results in any way.
+        """
+        return self._state.attributes[SERVICE_RANGEMAP]
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_RANGEMAP][item]
+
+    @property
+    @backend_parameter
+    def range_map_quality(self) -> RangeMapQuality:
+        """Type of the range map."""
+        return RangeMapQuality(self._state.attributes[SERVICE_RANGEMAP]["quality"])
+
+    @property
+    @backend_parameter
+    def range_map_center(self) -> RangeMapQuality:
+        """Type of the range map."""
+        return MapPoint(self._state.attributes[SERVICE_RANGEMAP]["center"])
+
+    @property
+    @backend_parameter
+    def range_maps(self) -> List[RangeMap]:
+        """Get the list range maps."""
+        range_maps_list = []
+        for dest in self._state.attributes[SERVICE_RANGEMAP]["rangemaps"]:
+            range_maps_list.append(RangeMap(dest))
+        return range_maps_list
diff --git a/bimmer_connected/state.py b/bimmer_connected/state.py
index 5aa4766..153fb4d 100644
--- a/bimmer_connected/state.py
+++ b/bimmer_connected/state.py
@@ -1,92 +1,23 @@
 """Models the state of a vehicle."""
 
-import datetime
 import logging
-from enum import Enum
+import datetime
 from typing import List
 
-from bimmer_connected.const import VEHICLE_STATUS_URL
-
-_LOGGER = logging.getLogger(__name__)
-
-
-LIDS = ['doorDriverFront', 'doorPassengerFront', 'doorDriverRear', 'doorPassengerRear',
-        'hood', 'trunk']
-
-WINDOWS = ['windowDriverFront', 'windowPassengerFront', 'windowDriverRear', 'windowPassengerRear', 'rearWindow',
-           'sunroof']
-
-
-class LidState(Enum):
-    """Possible states of the hatch, trunk, doors, windows, sun roof."""
-    CLOSED = 'CLOSED'
-    OPEN = 'OPEN'
-    OPEN_TILT = 'OPEN_TILT'
-    INTERMEDIATE = 'INTERMEDIATE'
-    INVALID = 'INVALID'
-
-
-class LockState(Enum):
-    """Possible states of the door locks."""
-    LOCKED = 'LOCKED'
-    SECURED = 'SECURED'
-    SELECTIVE_LOCKED = 'SELECTIVE_LOCKED'
-    UNLOCKED = 'UNLOCKED'
+from bimmer_connected.const import SERVICE_STATUS, VEHICLE_STATUS_URL, SERVICE_LAST_TRIP, \
+    VEHICLE_STATISTICS_LAST_TRIP_URL, SERVICE_ALL_TRIPS, VEHICLE_STATISTICS_ALL_TRIPS_URL, \
+    SERVICE_CHARGING_PROFILE, VEHICLE_CHARGING_PROFILE_URL, SERVICE_DESTINATIONS, VEHICLE_DESTINATIONS_URL, \
+    SERVICE_RANGEMAP, VEHICLE_RANGEMAP_URL
 
+from bimmer_connected.vehicle_status import VehicleStatus, LockState, ParkingLightState, ChargingState, \
+    CheckControlMessage, ConditionBasedServiceReport, Lid, Window
+from bimmer_connected.last_trip import LastTrip
+from bimmer_connected.all_trips import AllTrips
+from bimmer_connected.charging_profile import ChargingProfile
+from bimmer_connected.last_destinations import LastDestinations
+from bimmer_connected.range_maps import RangeMaps
 
-class ParkingLightState(Enum):
-    """Possible states of the parking lights"""
-    LEFT = 'LEFT'
-    RIGHT = 'RIGHT'
-    OFF = 'OFF'
-
-
-class ConditionBasedServiceStatus(Enum):
-    """Status of the condition based services."""
-    OK = 'OK'
-    OVERDUE = 'OVERDUE'
-    PENDING = 'PENDING'
-
-
-class ChargingState(Enum):
-    """Charging state of electric vehicle."""
-    CHARGING = 'CHARGING'
-    ERROR = 'ERROR'
-    FINISHED_FULLY_CHARGED = 'FINISHED_FULLY_CHARGED'
-    FINISHED_NOT_FULL = 'FINISHED_NOT_FULL'
-    INVALID = 'INVALID'
-    NOT_CHARGING = 'NOT_CHARGING'
-    WAITING_FOR_CHARGING = 'WAITING_FOR_CHARGING'
-
-
-class CheckControlMessage:
-    """Check control message sent from the server.
-
-    This class provides a nicer API than parsing the JSON format directly.
-    """
-
-    def __init__(self, ccm_dict: dict):
-        self._ccm_dict = ccm_dict
-
-    @property
-    def description_long(self) -> str:
-        """Long description of the check control message."""
-        return self._ccm_dict["ccmDescriptionLong"]
-
-    @property
-    def description_short(self) -> str:
-        """Short description of the check control message."""
-        return self._ccm_dict["ccmDescriptionShort"]
-
-    @property
-    def ccm_id(self) -> int:
-        """id of the check control message."""
-        return int(self._ccm_dict["ccmId"])
-
-    @property
-    def mileage(self) -> int:
-        """Mileage of the vehicle when the check control message appeared."""
-        return int(self._ccm_dict["ccmMileage"])
+_LOGGER = logging.getLogger(__name__)
 
 
 def backend_parameter(func):
@@ -97,7 +28,7 @@ def backend_parameter(func):
     def _func_wrapper(self: 'VehicleState', *args, **kwargs):
         # pylint: disable=protected-access
         if self._attributes is None:
-            raise ValueError('No data available for vehicle status!')
+            raise ValueError('No data available for vehicles state!')
         try:
             return func(self, *args, **kwargs)
         except KeyError:
@@ -106,14 +37,42 @@ def backend_parameter(func):
     return _func_wrapper
 
 
-class VehicleState:  # pylint: disable=too-many-public-methods
+class VehicleState:
     """Models the state of a vehicle."""
 
+    # pylint: disable=too-many-public-methods
+    # pylint: disable=too-many-instance-attributes
+    # Nine is reasonable in this case.
     def __init__(self, account, vehicle):
         """Constructor."""
         self._account = account
         self._vehicle = vehicle
         self._attributes = {}
+        self.vehicle_status = VehicleStatus(self)
+        self.all_trips = AllTrips(self)
+        self.charging_profile = ChargingProfile(self)
+        self.last_trip = LastTrip(self)
+        self.last_destinations = LastDestinations(self)
+        self.range_maps = RangeMaps(self)
+
+        self._url = {
+            SERVICE_STATUS: VEHICLE_STATUS_URL,
+            SERVICE_LAST_TRIP: VEHICLE_STATISTICS_LAST_TRIP_URL,
+            SERVICE_ALL_TRIPS: VEHICLE_STATISTICS_ALL_TRIPS_URL,
+            SERVICE_CHARGING_PROFILE: VEHICLE_CHARGING_PROFILE_URL,
+            SERVICE_DESTINATIONS: VEHICLE_DESTINATIONS_URL,
+            SERVICE_RANGEMAP: VEHICLE_RANGEMAP_URL}
+
+        self._key = {
+            SERVICE_STATUS: 'vehicleStatus',
+            SERVICE_LAST_TRIP: 'lastTrip',
+            SERVICE_ALL_TRIPS: 'allTrips',
+            SERVICE_CHARGING_PROFILE: 'weeklyPlanner',
+            SERVICE_DESTINATIONS: 'destinations',
+            SERVICE_RANGEMAP: 'rangemap'}
+
+        for service in self._url:
+            self._attributes[service] = {}
 
     def update_data(self) -> None:
         """Read new status data from the server."""
@@ -125,17 +84,18 @@ class VehicleState:  # pylint: disable=too-many-public-methods
             'dlat': self._vehicle.observer_latitude,
             'dlon': self._vehicle.observer_longitude,
         }
-        # Pre-NBT vehicles don't seem to have a status that can be reported
-        try:
-            response = self._account.send_request(
-                VEHICLE_STATUS_URL.format(server=self._account.server_url, vin=self._vehicle.vin), logfilename='status',
-                params=params)
-            attributes = response.json()['vehicleStatus']
-            _LOGGER.debug('received new data from connected drive')
-        except OSError:
-            attributes = {}
-            _LOGGER.debug('Unable to retrieve vehicle status from connected drive (car too old)')
-        self._attributes = attributes
+
+        for service in self._url:
+            try:
+                response = self._account.send_request(
+                    self._url[service].format(server=self._account.server_url, vin=self._vehicle.vin),
+                    logfilename=service, params=params)
+                self._attributes[service] = response.json()[self._key[service]]
+            except IOError:
+                _LOGGER.debug('Service %s failed', service)
+
+        _LOGGER.debug(self._attributes)
+        _LOGGER.debug('received new data from connected drive')
 
     @property
     @backend_parameter
@@ -146,11 +106,22 @@ class VehicleState:  # pylint: disable=too-many-public-methods
         """
         return self._attributes
 
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self.vehicle_status.attributes[item]
+
+    @staticmethod
+    def _parse_datetime(date_str: str) -> datetime.datetime:
+        """Convert a time string into datetime."""
+        date_format = "%Y-%m-%dT%H:%M:%S%z"
+        return datetime.datetime.strptime(date_str, date_format)
+
     @property
     @backend_parameter
     def timestamp(self) -> datetime.datetime:
         """Get the timestamp when the data was recorded."""
-        return self._parse_datetime(self._attributes['updateTime'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.timestamp instead")
+        return self.vehicle_status.timestamp
 
     @property
     @backend_parameter
@@ -160,12 +131,8 @@ class VehicleState:  # pylint: disable=too-many-public-methods
         Returns a tuple of (latitude, longitude).
         This only provides data, if the vehicle tracking is enabled!
         """
-        pos = self._attributes['position']
-        if not self.is_vehicle_tracking_enabled:
-            _LOGGER.warning('Positioning status is %s', pos['status'])
-            return None
-        pos = self._attributes['position']
-        return float(pos['lat']), float(pos['lon'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.gps_position instead")
+        return self.vehicle_status.gps_position
 
     @property
     @backend_parameter
@@ -174,7 +141,8 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         The server return "OK" if tracking is enabled and "DRIVER_DISABLED" if it is disabled in the vehicle.
         """
-        return self._attributes['position']['status'] == 'OK'
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.is_vehicle_tracking_enabled instead")
+        return bool(self.vehicle_status.is_vehicle_tracking_enabled)
 
     @property
     @backend_parameter
@@ -183,7 +151,8 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        return int(self._attributes['mileage'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.mileage instead")
+        return self.vehicle_status.mileage
 
     @property
     @backend_parameter
@@ -192,7 +161,8 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        return int(self._attributes['remainingRangeFuel'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.remaining_range_fuel instead")
+        return self.vehicle_status.remaining_range_fuel
 
     @property
     @backend_parameter
@@ -201,85 +171,92 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         Returns a tuple of (value, unit_of_measurement)
         """
-        return int(self._attributes['remainingFuel'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.remaining_fuel instead")
+        return self.vehicle_status.remaining_fuel
 
     @property
     @backend_parameter
-    def lids(self) -> List['Lid']:
+    def lids(self) -> List[Lid]:
         """Get all lids (doors+hatch+trunk) of the car."""
-        result = []
-        for lid in LIDS:
-            if lid in self._attributes and self._attributes[lid] != LidState.INVALID.value:
-                result.append(Lid(self, lid))
-        return result
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.lids instead")
+        return self.vehicle_status.lids
 
     @property
-    def open_lids(self) -> List['Lid']:
+    @backend_parameter
+    def open_lids(self) -> List[Lid]:
         """Get all open lids of the car."""
-        return [lid for lid in self.lids if not lid.is_closed]
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.open_lids instead")
+        return self.vehicle_status.open_lids
 
     @property
+    @backend_parameter
     def all_lids_closed(self) -> bool:
         """Check if all lids are closed."""
-        return len(list(self.open_lids)) == 0
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.all_lids_closed instead")
+        return self.vehicle_status.all_lids_closed
 
     @property
     @backend_parameter
-    def windows(self) -> List['Window']:
+    def windows(self) -> List[Window]:
         """Get all windows (doors+sun roof) of the car."""
-        result = []
-        for window in WINDOWS:
-            if window in self._attributes and self._attributes[window] != LidState.INVALID.value:
-                result.append(Window(self, window))
-        return result
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.windows instead")
+        return self.vehicle_status.windows
 
     @property
-    def open_windows(self) -> List['Window']:
+    @backend_parameter
+    def open_windows(self) -> List[Window]:
         """Get all open windows of the car."""
-        return [lid for lid in self.windows if not lid.is_closed]
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.open_windows instead")
+        return self.vehicle_status.open_windows
 
     @property
+    @backend_parameter
     def all_windows_closed(self) -> bool:
         """Check if all windows are closed."""
-        return len(list(self.open_windows)) == 0
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.all_windows_closed instead")
+        return self.vehicle_status.all_windows_closed
 
     @property
     @backend_parameter
     def door_lock_state(self) -> LockState:
         """Get state of the door locks."""
-        return LockState(self._attributes['doorLockState'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.all_windows_closed instead")
+        return self.vehicle_status.door_lock_state
 
     @property
     @backend_parameter
     def last_update_reason(self) -> str:
-        """The reason for the last state update."""
-        return self._attributes['updateReason']
+        """The reason for the last state update"""
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.last_update_reason instead")
+        return self.vehicle_status.last_update_reason
 
     @property
     @backend_parameter
     def last_charging_end_result(self) -> str:
-        """Get the last charging end result."""
-        return self._attributes['lastChargingEndResult']
+        """Get the last charging end result"""
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.last_charging_end_result instead")
+        return self.vehicle_status.last_charging_end_result
 
     @property
     @backend_parameter
     def connection_status(self) -> str:
-        """Get status of the connection."""
-        return self._attributes['connectionStatus']
+        """Get status of the connection"""
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.connection_status instead")
+        return self.vehicle_status.connection_status
 
     @property
     @backend_parameter
-    def condition_based_services(self) -> List['ConditionBasedServiceReport']:
+    def condition_based_services(self) -> List[ConditionBasedServiceReport]:
         """Get status of the condition based services."""
-        return [ConditionBasedServiceReport(s) for s in self._attributes['cbsData']]
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.condition_based_services instead")
+        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"."""
-        for cbs in self.condition_based_services:
-            if cbs.state != ConditionBasedServiceStatus.OK:
-                return False
-        return True
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.are_all_cbs_ok instead")
+        return bool(self.vehicle_status.are_all_cbs_ok)
 
     @property
     @backend_parameter
@@ -288,34 +265,25 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         :returns None if status is unknown.
         """
-        return ParkingLightState(self.attributes['parkingLight'])
+        _LOGGER.warning("Funcion 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.
 
         :returns None if status is unknown.
         """
-        lights = self.parking_lights
-        if lights is None:
-            return None
-        return lights != ParkingLightState.OFF
-
-    @staticmethod
-    def _parse_datetime(date_str: str) -> datetime.datetime:
-        """Convert a time string into datetime."""
-        date_format = "%Y-%m-%dT%H:%M:%S%z"
-        return datetime.datetime.strptime(date_str, date_format)
-
-    def __getattr__(self, item):
-        """Generic get function for all backend attributes."""
-        return self._attributes.get(item)
+        _LOGGER.warning("Funcion 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."""
-        return int(self._attributes['remainingRangeElectric'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.remaining_range_electric instead")
+        return self.vehicle_status.remaining_range_electric
 
     @property
     @backend_parameter
@@ -324,122 +292,47 @@ class VehicleState:  # pylint: disable=too-many-public-methods
 
         That is electrical range + fuel range.
         """
-        result = 0
-        if self.remaining_range_electric is not None:
-            result += self.remaining_range_electric
-        if self.remaining_range_fuel is not None:
-            result += self.remaining_range_fuel
-        return result
+        _LOGGER.warning("Funcion 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."""
-        return int(self._attributes['maxRangeElectric'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.max_range_electric instead")
+        return self.vehicle_status.max_range_electric
 
     @property
     @backend_parameter
     def charging_status(self) -> ChargingState:
         """Charging state of the vehicle."""
-        state = self._attributes['chargingStatus']
-        return ChargingState(state)
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.charging_status instead")
+        return self.vehicle_status.charging_status
 
     @property
     @backend_parameter
     def charging_time_remaining(self) -> datetime.timedelta:
         """Get the remaining charging time."""
-        minutes = self._attributes['chargingTimeRemaining']
-        return datetime.timedelta(minutes=minutes)
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.charging_time_remaining instead")
+        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."""
-        return int(self._attributes['chargingLevelHv'])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.charging_level_hv instead")
+        return self.vehicle_status.charging_level_hv
 
     @property
     @backend_parameter
     def check_control_messages(self) -> List[CheckControlMessage]:
         """List of check control messages."""
-        # TO DO change this in HA binary_sensor.py first
-        # messages = self._attributes.get('checkControlMessages', [])
-        # return [CheckControlMessage(m) for m in messages]
-        return self._attributes.get('checkControlMessages', [])
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.check_control_messages instead")
+        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."""
-        return len(self.check_control_messages) > 0
-
-
-class Lid:  # pylint: disable=too-few-public-methods
-    """A lid of the vehicle.
-
-    Lids are: Doors + Trunk + Hatch
-    """
-
-    def __init__(self, vehicle_state: VehicleState, name: str):
-        #: name of the lid
-        self.name = name
-        self._vehicle_state = vehicle_state
-
-    @property
-    def state(self):
-        """Get the current state of the lid."""
-        return LidState(getattr(self._vehicle_state, self.name))
-
-    @property
-    def is_closed(self) -> bool:
-        """Check if the lid is closed."""
-        return self.state == LidState.CLOSED
-
-    def __str__(self) -> str:
-        return '{}: {}'.format(self.name, self.state.value)
-
-
-class Window(Lid):  # pylint: disable=too-few-public-methods
-    """A window of the vehicle.
-
-    A window can be a normal window of the car or the sun roof.
-    """
-
-
-class ConditionBasedServiceReport:  # pylint: disable=too-few-public-methods
-    """Entry in the list of condition based services."""
-
-    def __init__(self, data: dict):
-
-        #: date when the service is due
-        self.due_date = self._parse_date(data.get('cbsDueDate'))
-
-        #: status of the service
-        self.state = ConditionBasedServiceStatus(data['cbsState'])
-
-        #: service type
-        self.service_type = data['cbsType']
-
-        #: distance when the service is due
-        self.due_distance = None
-        if 'cbsRemainingMileage' in data:
-            self.due_distance = int(data['cbsRemainingMileage'])
-
-        #: description of the required service
-        self.description = data['cbsDescription']
-
-    @staticmethod
-    def _parse_date(datestr: str) -> datetime.datetime:
-        if datestr is None:
-            return None
-        formats = [
-            '%Y-%m',
-            '%m.%Y',
-        ]
-        for date_format in formats:
-            try:
-                date = datetime.datetime.strptime(datestr, date_format)
-                return date.replace(day=1)
-            except ValueError:
-                pass
-        _LOGGER.error('Unknown time format for CBS: %s', datestr)
-        return None
+        _LOGGER.warning("Funcion deprecated use state.vehicle_status.has_check_control_messages instead")
+        return bool(self.vehicle_status.has_check_control_messages)
diff --git a/bimmer_connected/vehicle.py b/bimmer_connected/vehicle.py
index da50ea8..f5acfe7 100644
--- a/bimmer_connected/vehicle.py
+++ b/bimmer_connected/vehicle.py
@@ -3,7 +3,8 @@ from enum import Enum
 import logging
 from typing import List
 
-from bimmer_connected.state import VehicleState, WINDOWS, LIDS
+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
 
diff --git a/bimmer_connected/vehicle_status.py b/bimmer_connected/vehicle_status.py
new file mode 100644
index 0000000..9079a14
--- /dev/null
+++ b/bimmer_connected/vehicle_status.py
@@ -0,0 +1,423 @@
+"""Models the state of a vehicle."""
+
+import datetime
+import logging
+from enum import Enum
+from typing import List
+
+from bimmer_connected.const import SERVICE_STATUS
+
+_LOGGER = logging.getLogger(__name__)
+
+
+LIDS = ['doorDriverFront', 'doorPassengerFront', 'doorDriverRear', 'doorPassengerRear',
+        'hood', 'trunk']
+
+WINDOWS = ['windowDriverFront', 'windowPassengerFront', 'windowDriverRear', 'windowPassengerRear', 'rearWindow',
+           'sunroof']
+
+
+class LidState(Enum):
+    """Possible states of the hatch, trunk, doors, windows, sun roof."""
+    CLOSED = 'CLOSED'
+    OPEN = 'OPEN'
+    OPEN_TILT = 'OPEN_TILT'
+    INTERMEDIATE = 'INTERMEDIATE'
+    INVALID = 'INVALID'
+
+
+class LockState(Enum):
+    """Possible states of the door locks."""
+    LOCKED = 'LOCKED'
+    SECURED = 'SECURED'
+    SELECTIVE_LOCKED = 'SELECTIVE_LOCKED'
+    UNLOCKED = 'UNLOCKED'
+
+
+class ParkingLightState(Enum):
+    """Possible states of the parking lights"""
+    LEFT = 'LEFT'
+    RIGHT = 'RIGHT'
+    OFF = 'OFF'
+
+
+class ConditionBasedServiceStatus(Enum):
+    """Status of the condition based services."""
+    OK = 'OK'
+    OVERDUE = 'OVERDUE'
+    PENDING = 'PENDING'
+
+
+class ChargingState(Enum):
+    """Charging state of electric vehicle."""
+    CHARGING = 'CHARGING'
+    ERROR = 'ERROR'
+    FINISHED_FULLY_CHARGED = 'FINISHED_FULLY_CHARGED'
+    FINISHED_NOT_FULL = 'FINISHED_NOT_FULL'
+    INVALID = 'INVALID'
+    NOT_CHARGING = 'NOT_CHARGING'
+    WAITING_FOR_CHARGING = 'WAITING_FOR_CHARGING'
+
+
+class CheckControlMessage:
+    """Check control message sent from the server.
+
+    This class provides a nicer API than parsing the JSON format directly.
+    """
+
+    def __init__(self, ccm_dict: dict):
+        self._ccm_dict = ccm_dict
+
+    @property
+    def description_long(self) -> str:
+        """Long description of the check control message."""
+        return self._ccm_dict["ccmDescriptionLong"]
+
+    @property
+    def description_short(self) -> str:
+        """Short description of the check control message."""
+        return self._ccm_dict["ccmDescriptionShort"]
+
+    @property
+    def ccm_id(self) -> int:
+        """id of the check control message."""
+        return int(self._ccm_dict["ccmId"])
+
+    @property
+    def mileage(self) -> int:
+        """Mileage of the vehicle when the check control message appeared."""
+        return int(self._ccm_dict["ccmMileage"])
+
+
+def backend_parameter(func):
+    """Decorator for parameters reading data from the backend.
+
+    Errors are handled in a default way.
+    """
+    def _func_wrapper(self: 'VehicleStatus', *args, **kwargs):
+        # pylint: disable=protected-access
+        if self._state.attributes[SERVICE_STATUS] is None:
+            raise ValueError('No data available for vehicle status!')
+        try:
+            return func(self, *args, **kwargs)
+        except KeyError:
+            _LOGGER.debug('No data available for attribute %s!', str(func))
+            return None
+    return _func_wrapper
+
+
+class VehicleStatus:  # pylint: disable=too-many-public-methods
+    """Models the status of a vehicle."""
+
+    def __init__(self, state):
+        """Constructor."""
+        self._state = state
+
+    @property
+    @backend_parameter
+    def attributes(self) -> dict:
+        """Retrieve all attributes from the sever.
+
+        This does not parse the results in any way.
+        """
+        return self._state.attributes[SERVICE_STATUS]
+
+    @property
+    @backend_parameter
+    def timestamp(self) -> datetime.datetime:
+        """Get the timestamp when the data was recorded."""
+        return self._parse_datetime(self._state.attributes[SERVICE_STATUS]['updateTime'])
+
+    @property
+    @backend_parameter
+    def gps_position(self) -> (float, float):
+        """Get the last known position of the vehicle.
+
+        Returns a tuple of (latitude, longitude).
+        This only provides data, if the vehicle tracking is enabled!
+        """
+        pos = self._state.attributes[SERVICE_STATUS]['position']
+        if not self.is_vehicle_tracking_enabled:
+            _LOGGER.warning('Positioning status is %s', pos['status'])
+            return None
+        pos = self._state.attributes[SERVICE_STATUS]['position']
+        return float(pos['lat']), float(pos['lon'])
+
+    @property
+    @backend_parameter
+    def is_vehicle_tracking_enabled(self) -> bool:
+        """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.
+        """
+        return self._state.attributes[SERVICE_STATUS]['position']['status'] == 'OK'
+
+    @property
+    @backend_parameter
+    def mileage(self) -> int:
+        """Get the mileage of the vehicle.
+
+        Returns a tuple of (value, unit_of_measurement)
+        """
+        return int(self._state.attributes[SERVICE_STATUS]['mileage'])
+
+    @property
+    @backend_parameter
+    def remaining_range_fuel(self) -> int:
+        """Get the remaining range of the vehicle on fuel.
+
+        Returns a tuple of (value, unit_of_measurement)
+        """
+        return int(self._state.attributes[SERVICE_STATUS]['remainingRangeFuel'])
+
+    @property
+    @backend_parameter
+    def remaining_fuel(self) -> int:
+        """Get the remaining fuel of the vehicle.
+
+        Returns a tuple of (value, unit_of_measurement)
+        """
+        return int(self._state.attributes[SERVICE_STATUS]['remainingFuel'])
+
+    @property
+    @backend_parameter
+    def lids(self) -> List['Lid']:
+        """Get all lids (doors+hatch+trunk) of the car."""
+        result = []
+        for lid in LIDS:
+            if lid in self._state.attributes[SERVICE_STATUS] and \
+                    self._state.attributes[SERVICE_STATUS][lid] != LidState.INVALID.value:
+                result.append(Lid(self, lid))
+        return result
+
+    @property
+    def open_lids(self) -> List['Lid']:
+        """Get all open lids of the car."""
+        return [lid for lid in self.lids if not lid.is_closed]
+
+    @property
+    def all_lids_closed(self) -> bool:
+        """Check if all lids are closed."""
+        return len(list(self.open_lids)) == 0
+
+    @property
+    @backend_parameter
+    def windows(self) -> List['Window']:
+        """Get all windows (doors+sun roof) of the car."""
+        result = []
+        for window in WINDOWS:
+            if window in self._state.attributes[SERVICE_STATUS] and \
+                    self._state.attributes[SERVICE_STATUS][window] != LidState.INVALID.value:
+                result.append(Window(self, window))
+        return result
+
+    @property
+    def open_windows(self) -> List['Window']:
+        """Get all open windows of the car."""
+        return [lid for lid in self.windows if not lid.is_closed]
+
+    @property
+    def all_windows_closed(self) -> bool:
+        """Check if all windows are closed."""
+        return len(list(self.open_windows)) == 0
+
+    @property
+    @backend_parameter
+    def door_lock_state(self) -> LockState:
+        """Get state of the door locks."""
+        return LockState(self._state.attributes[SERVICE_STATUS]['doorLockState'])
+
+    @property
+    @backend_parameter
+    def last_update_reason(self) -> str:
+        """The reason for the last state update"""
+        return self._state.attributes[SERVICE_STATUS]['updateReason']
+
+    @property
+    @backend_parameter
+    def last_charging_end_result(self) -> str:
+        """Get the last charging end result"""
+        return self._state.attributes[SERVICE_STATUS]['lastChargingEndResult']
+
+    @property
+    @backend_parameter
+    def connection_status(self) -> str:
+        """Get status of the connection"""
+        return self._state.attributes[SERVICE_STATUS]['connectionStatus']
+
+    @property
+    @backend_parameter
+    def condition_based_services(self) -> List['ConditionBasedServiceReport']:
+        """Get status of the condition based services."""
+        return [ConditionBasedServiceReport(s) for s in self._state.attributes[SERVICE_STATUS]['cbsData']]
+
+    @property
+    def are_all_cbs_ok(self) -> bool:
+        """Check if the status of all condition based services is "OK"."""
+        for cbs in self.condition_based_services:
+            if cbs.state != ConditionBasedServiceStatus.OK:
+                return False
+        return True
+
+    @property
+    @backend_parameter
+    def parking_lights(self) -> ParkingLightState:
+        """Get status of parking lights.
+
+        :returns None if status is unknown.
+        """
+        return ParkingLightState(self._state.attributes[SERVICE_STATUS]['parkingLight'])
+
+    @property
+    def are_parking_lights_on(self) -> bool:
+        """Get status of parking lights.
+
+        :returns None if status is unknown.
+        """
+        lights = self.parking_lights
+        if lights is None:
+            return None
+        return lights != ParkingLightState.OFF
+
+    @staticmethod
+    def _parse_datetime(date_str: str) -> datetime.datetime:
+        """Convert a time string into datetime."""
+        date_format = "%Y-%m-%dT%H:%M:%S%z"
+        return datetime.datetime.strptime(date_str, date_format)
+
+    def __getattr__(self, item):
+        """Generic get function for all backend attributes."""
+        return self._state.attributes[SERVICE_STATUS][item]
+
+    @property
+    @backend_parameter
+    def remaining_range_electric(self) -> int:
+        """Remaining range on battery, in kilometers."""
+        return int(self._state.attributes[SERVICE_STATUS]['remainingRangeElectric'])
+
+    @property
+    @backend_parameter
+    def remaining_range_total(self) -> int:
+        """Get the total remaining range of the vehicle in kilometers.
+
+        That is electrical range + fuel range.
+        """
+        result = 0
+        if self.remaining_range_electric is not None:
+            result += self.remaining_range_electric
+        if self.remaining_range_fuel is not None:
+            result += self.remaining_range_fuel
+        return result
+
+    @property
+    @backend_parameter
+    def max_range_electric(self) -> int:
+        """ This can change with driving style and temperature in kilometers."""
+        return int(self._state.attributes[SERVICE_STATUS]['maxRangeElectric'])
+
+    @property
+    @backend_parameter
+    def charging_status(self) -> ChargingState:
+        """Charging state of the vehicle."""
+        state = self._state.attributes[SERVICE_STATUS]['chargingStatus']
+        return ChargingState(state)
+
+    @property
+    @backend_parameter
+    def charging_time_remaining(self) -> datetime.timedelta:
+        """Get the remaining charging time."""
+        minutes = self._state.attributes[SERVICE_STATUS]['chargingTimeRemaining']
+        return datetime.timedelta(minutes=minutes)
+
+    @property
+    @backend_parameter
+    def charging_level_hv(self) -> int:
+        """State of charge of the high voltage battery in percent."""
+        return int(self._state.attributes[SERVICE_STATUS]['chargingLevelHv'])
+
+    @property
+    @backend_parameter
+    def check_control_messages(self) -> List[CheckControlMessage]:
+        """List of check control messages."""
+        # TO DO change this in HA binary_sensor.py first
+        # messages = self._state.attributes[SERVICE_STATUS]['STATUS'].get('checkControlMessages', [])
+        # return [CheckControlMessage(m) for m in messages]
+        return self._state.attributes[SERVICE_STATUS].get('checkControlMessages', [])
+
+    @property
+    @backend_parameter
+    def has_check_control_messages(self) -> bool:
+        """Return true if any check control message is present."""
+        return len(self.check_control_messages) > 0
+
+
+class Lid:  # pylint: disable=too-few-public-methods
+    """A lid of the vehicle.
+
+    Lids are: Doors + Trunk + Hatch
+    """
+
+    def __init__(self, vehicle_status: VehicleStatus, name: str):
+        #: name of the lid
+        self.name = name
+        self._vehicle_status = vehicle_status
+
+    @property
+    def state(self):
+        """Get the current state of the lid."""
+        return LidState(getattr(self._vehicle_status, self.name))
+
+    @property
+    def is_closed(self) -> bool:
+        """Check if the lid is closed."""
+        return self.state == LidState.CLOSED
+
+    def __str__(self) -> str:
+        return '{}: {}'.format(self.name, self._vehicle_status)
+
+
+class Window(Lid):  # pylint: disable=too-few-public-methods
+    """A window of the vehicle.
+
+    A window can be a normal window of the car or the sun roof.
+    """
+
+
+class ConditionBasedServiceReport:  # pylint: disable=too-few-public-methods
+    """Entry in the list of condition based services."""
+
+    def __init__(self, data: dict):
+
+        #: date when the service is due
+        self.due_date = self._parse_date(data.get('cbsDueDate'))
+
+        #: status of the service
+        self.state = ConditionBasedServiceStatus(data['cbsState'])
+
+        #: service type
+        self.service_type = data['cbsType']
+
+        #: distance when the service is due
+        self.due_distance = None
+        if 'cbsRemainingMileage' in data:
+            self.due_distance = int(data['cbsRemainingMileage'])
+
+        #: description of the required service
+        self.description = data['cbsDescription']
+
+    @staticmethod
+    def _parse_date(datestr: str) -> datetime.datetime:
+        if datestr is None:
+            return None
+        formats = [
+            '%Y-%m',
+            '%m.%Y',
+        ]
+        for date_format in formats:
+            try:
+                date = datetime.datetime.strptime(datestr, date_format)
+                return date.replace(day=1)
+            except ValueError:
+                pass
+        _LOGGER.error('Unknown time format for CBS: %s', datestr)
+        return None


Test patch:
diff --git a/test/responses/I01_NOREX/all_trips.json b/test/responses/I01_NOREX/all_trips.json
new file mode 100644
index 0000000..ce0fe67
--- /dev/null
+++ b/test/responses/I01_NOREX/all_trips.json
@@ -0,0 +1,40 @@
+{
+    "allTrips": {
+        "avgElectricConsumption": {
+            "communityLow": 0,
+            "communityAverage": 16.33,
+            "communityHigh": 35.53,
+            "userAverage": 14.76
+        },
+        "avgRecuperation": {
+            "communityLow": 0,
+            "communityAverage": 3.76,
+            "communityHigh": 14.03,
+            "userAverage": 2.3
+        },
+        "chargecycleRange": {
+            "communityAverage": 121.58,
+            "communityHigh": 200,
+            "userAverage": 72.62,
+            "userHigh": 135,
+            "userCurrentChargeCycle": 60
+        },
+        "totalElectricDistance": {
+            "communityLow": 1,
+            "communityAverage": 12293.65,
+            "communityHigh": 77533.6,
+            "userTotal": 3158.66
+        },
+        "avgCombinedConsumption": {
+            "communityLow": 0,
+            "communityAverage": 1.21,
+            "communityHigh": 6.2,
+            "userAverage": 0.36
+        },
+        "batterySizeMax": 35820,
+        "savedCO2": 87.58,
+        "savedCO2greenEnergy": 515.177,
+        "totalSavedFuel": 0,
+        "resetDate": "1970-01-01T01:00:00+0100"
+    }
+}
\ No newline at end of file
diff --git a/test/responses/I01_NOREX/charging_profile.json b/test/responses/I01_NOREX/charging_profile.json
new file mode 100644
index 0000000..d5aa3f5
--- /dev/null
+++ b/test/responses/I01_NOREX/charging_profile.json
@@ -0,0 +1,40 @@
+{
+   "weeklyPlanner":{
+      "climatizationEnabled":true,
+      "chargingMode":"DELAYED_CHARGING",
+      "chargingPreferences":"CHARGING_WINDOW",
+      "timer1":{
+         "departureTime":"07:30",
+         "timerEnabled":true,
+         "weekdays":[
+            "MONDAY"
+         ]
+      },
+      "timer2":{
+         "departureTime":"13:00",
+         "timerEnabled":false,
+         "weekdays":[
+            "SATURDAY"
+         ]
+      },
+      "timer3":{
+         "departureTime":"08:00",
+         "timerEnabled":false,
+         "weekdays":[
+
+         ]
+      },
+      "overrideTimer":{
+         "departureTime":"07:30",
+         "timerEnabled":false,
+         "weekdays":[
+            "MONDAY"
+         ]
+      },
+      "preferredChargingWindow":{
+         "enabled":true,
+         "startTime":"05:02",
+         "endTime":"17:31"
+      }
+   }
+}
\ No newline at end of file
diff --git a/test/responses/I01_NOREX/last_destinations.json b/test/responses/I01_NOREX/last_destinations.json
new file mode 100644
index 0000000..d4c6cd3
--- /dev/null
+++ b/test/responses/I01_NOREX/last_destinations.json
@@ -0,0 +1,13 @@
+{
+   "destinations":[
+      {
+         "lat":51.53053283691406,
+         "lon":-0.08362331241369247,
+         "country":"UNITED KINGDOM",
+         "city":"LONDON",
+         "street":"PITFIELD STREET",
+         "type":"DESTINATION",
+         "createdAt":"2015-09-25T08:06:11+0200"
+      }
+   ]
+}
\ No newline at end of file
diff --git a/test/responses/I01_NOREX/last_trip.json b/test/responses/I01_NOREX/last_trip.json
new file mode 100644
index 0000000..60cc5ed
--- /dev/null
+++ b/test/responses/I01_NOREX/last_trip.json
@@ -0,0 +1,19 @@
+{
+   "lastTrip":{
+      "efficiencyValue":0.53,
+      "totalDistance":141,
+      "electricDistance":100.1,
+      "avgElectricConsumption":16.6,
+      "avgRecuperation":2,
+      "drivingModeValue":0,
+      "accelerationValue":0.39,
+      "anticipationValue":0.81,
+      "totalConsumptionValue":0.79,
+      "auxiliaryConsumptionValue":0.66,
+      "avgCombinedConsumption":1.9,
+      "electricDistanceRatio":71,
+      "savedFuel":0,
+      "date":"2015-12-01T20:44:00+0100",
+      "duration":124
+   }
+}
\ No newline at end of file
diff --git a/test/responses/I01_NOREX/range_maps.json b/test/responses/I01_NOREX/range_maps.json
new file mode 100644
index 0000000..1c9fe4d
--- /dev/null
+++ b/test/responses/I01_NOREX/range_maps.json
@@ -0,0 +1,37 @@
+{
+    "rangemap": {
+        "center": {
+            "lat": 51.123456,
+            "lon": -1.2345678
+        },
+        "quality": "AVERAGE",
+        "rangemaps": [
+            {
+                "type": "ECO_PRO_PLUS",
+                "polyline": [
+                    {
+                        "lat": 51.6991281509399,
+                        "lon": -2.00423240661621
+                    },
+                    {
+                        "lat": 51.6909098625183,
+                        "lon": -1.91526889801025
+                    }
+                ]
+            },
+            {
+                "type": "COMFORT",
+                "polyline": [
+                    {
+                        "lat": 51.7212295532227,
+                        "lon": -1.7363977432251
+                    },
+                    {
+                        "lat": 51.6991496086121,
+                        "lon": -1.73077583312988
+                    }
+                 ]
+            }
+        ]
+    }
+}
diff --git a/test/test_all_trips.py b/test/test_all_trips.py
new file mode 100644
index 0000000..ff0f7d6
--- /dev/null
+++ b/test/test_all_trips.py
@@ -0,0 +1,52 @@
+"""Test for AllTrips."""
+import unittest
+from unittest import mock
+from test import load_response_json
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_ALL_TRIPS
+
+I01_TEST_DATA = load_response_json('I01_NOREX/all_trips.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for VehicleState."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_ALL_TRIPS] = I01_TEST_DATA['allTrips']
+        self.assertEqual('1970-01-01T01:00:00+0100', state.all_trips.reset_date)
+        self.assertEqual(35820, state.all_trips.battery_size_max)
+        self.assertEqual(87.58, state.all_trips.saved_co2)
+        self.assertEqual(515.177, state.all_trips.saved_co2_green_energy)
+        self.assertEqual(0, state.all_trips.total_saved_fuel)
+
+        self.assertEqual(0, state.all_trips.average_electric_consumption.community_low)
+        self.assertEqual(16.33, state.all_trips.average_electric_consumption.community_average)
+        self.assertEqual(35.53, state.all_trips.average_electric_consumption.community_high)
+        self.assertEqual(14.76, state.all_trips.average_electric_consumption.user_average)
+
+        self.assertEqual(0, state.all_trips.average_recopuration.community_low)
+        self.assertEqual(3.76, state.all_trips.average_recopuration.community_average)
+        self.assertEqual(14.03, state.all_trips.average_recopuration.community_high)
+        self.assertEqual(2.3, state.all_trips.average_recopuration.user_average)
+
+        self.assertEqual(121.58, state.all_trips.chargecycle_range.community_average)
+        self.assertEqual(200, state.all_trips.chargecycle_range.community_high)
+        self.assertEqual(72.62, state.all_trips.chargecycle_range.user_average)
+        self.assertEqual(135, state.all_trips.chargecycle_range.user_high)
+        self.assertEqual(60, state.all_trips.chargecycle_range.user_current_charge_cycle)
+
+        self.assertEqual(1, state.all_trips.total_electric_distance.community_low)
+        self.assertEqual(12293.65, state.all_trips.total_electric_distance.community_average)
+        self.assertEqual(77533.6, state.all_trips.total_electric_distance.community_high)
+        self.assertEqual(3158.66, state.all_trips.total_electric_distance.user_total)
+
+        self.assertEqual(0, state.all_trips.average_combined_consumption.community_low)
+        self.assertEqual(1.21, state.all_trips.average_combined_consumption.community_average)
+        self.assertEqual(6.2, state.all_trips.average_combined_consumption.community_high)
+        self.assertEqual(0.36, state.all_trips.average_combined_consumption.user_average)
diff --git a/test/test_charging_profile.py b/test/test_charging_profile.py
new file mode 100644
index 0000000..5f9dc20
--- /dev/null
+++ b/test/test_charging_profile.py
@@ -0,0 +1,33 @@
+"""Test for ChargingProfile"""
+import unittest
+from unittest import mock
+from test import load_response_json
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_CHARGING_PROFILE
+from bimmer_connected.charging_profile import ChargingMode, ChargingPreferences, TimerTypes
+
+I01_TEST_DATA = load_response_json('I01_NOREX/charging_profile.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for ChargingProfile."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_CHARGING_PROFILE] = I01_TEST_DATA['weeklyPlanner']
+        self.assertTrue(state.charging_profile.is_pre_entry_climatization_enabled)
+        self.assertEqual(ChargingMode.DELAYED_CHARGING, state.charging_profile.charging_mode)
+        self.assertEqual(ChargingPreferences.CHARGING_WINDOW, state.charging_profile.charging_preferences)
+
+        self.assertTrue(state.charging_profile.pre_entry_climatization_timer[TimerTypes.TIMER_1].timer_enabled)
+        self.assertEqual('07:30',
+                         state.charging_profile.pre_entry_climatization_timer[TimerTypes.TIMER_1].departure_time)
+        self.assertEqual('MONDAY', state.charging_profile.pre_entry_climatization_timer[TimerTypes.TIMER_1].weekdays[0])
+
+        self.assertEqual('05:02', state.charging_profile.prefered_charging_window.start_time)
+        self.assertEqual('17:31', state.charging_profile.prefered_charging_window.end_time)
diff --git a/test/test_last_destinations.py b/test/test_last_destinations.py
new file mode 100644
index 0000000..7db6990
--- /dev/null
+++ b/test/test_last_destinations.py
@@ -0,0 +1,29 @@
+"""Test for LastDestinations"""
+import unittest
+from unittest import mock
+from test import load_response_json
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_DESTINATIONS
+from bimmer_connected.last_destinations import DestinationType
+
+I01_TEST_DATA = load_response_json('I01_NOREX/last_destinations.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for LastDestinations."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_DESTINATIONS] = I01_TEST_DATA['destinations']
+        self.assertEqual(DestinationType.DESTINATION, state.last_destinations.last_destinations[0].destination_type)
+        self.assertEqual(51.53053283691406, state.last_destinations.last_destinations[0].latitude)
+        self.assertEqual(-0.08362331241369247, state.last_destinations.last_destinations[0].longitude)
+        self.assertEqual('UNITED KINGDOM', state.last_destinations.last_destinations[0].country)
+        self.assertEqual('LONDON', state.last_destinations.last_destinations[0].city)
+        self.assertEqual('PITFIELD STREET', state.last_destinations.last_destinations[0].street)
+        self.assertEqual('2015-09-25T08:06:11+0200', state.last_destinations.last_destinations[0].created_at)
diff --git a/test/test_last_trip.py b/test/test_last_trip.py
new file mode 100644
index 0000000..2d603a1
--- /dev/null
+++ b/test/test_last_trip.py
@@ -0,0 +1,36 @@
+"""Test for LastTrip"""
+import unittest
+from unittest import mock
+from test import load_response_json
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_LAST_TRIP
+
+I01_TEST_DATA = load_response_json('I01_NOREX/last_trip.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for LastTrip."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_LAST_TRIP] = I01_TEST_DATA['lastTrip']
+        self.assertEqual(0.53, state.last_trip.efficiencyValue)
+        self.assertEqual(141, state.last_trip.total_distance)
+        self.assertEqual(100.1, state.last_trip.electric_distance)
+        self.assertEqual(16.6, state.last_trip.averege_electric_consumption)
+        self.assertEqual(2, state.last_trip.averege_recuperation)
+        self.assertEqual(0, state.last_trip.driving_mode_value)
+        self.assertEqual(0.39, state.last_trip.acceleration_value)
+        self.assertEqual(0.81, state.last_trip.anticipation_value)
+        self.assertEqual(0.79, state.last_trip.total_consumption_value)
+        self.assertEqual(0.66, state.last_trip.auxiliary_consumption_value)
+        self.assertEqual(1.9, state.last_trip.averege_combined_consumption)
+        self.assertEqual(71, state.last_trip.electric_distance_ratio)
+        self.assertEqual(0, state.last_trip.saved_fuel)
+        self.assertEqual('2015-12-01T20:44:00+0100', state.last_trip.date)
+        self.assertEqual(124, state.last_trip.duration)
diff --git a/test/test_range_maps.py b/test/test_range_maps.py
new file mode 100644
index 0000000..4fc82be
--- /dev/null
+++ b/test/test_range_maps.py
@@ -0,0 +1,28 @@
+"""Test for RangeMaps"""
+import unittest
+from unittest import mock
+from test import load_response_json
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_RANGEMAP
+from bimmer_connected.range_maps import RangeMapType, RangeMapQuality
+
+I01_TEST_DATA = load_response_json('I01_NOREX/range_maps.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for LastDestinations."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_RANGEMAP] = I01_TEST_DATA['rangemap']
+        self.assertEqual(RangeMapQuality.AVERAGE, state.range_maps.range_map_quality)
+        self.assertEqual(51.123456, state.range_maps.range_map_center.latitude)
+        self.assertEqual(-1.2345678, state.range_maps.range_map_center.longitude)
+        self.assertEqual(RangeMapType.ECO_PRO_PLUS, state.range_maps.range_maps[0].range_map_type)
+        self.assertEqual(51.6991281509399, state.range_maps.range_maps[0].polyline[0].latitude)
+        self.assertEqual(-2.00423240661621, state.range_maps.range_maps[0].polyline[0].longitude)
diff --git a/test/test_state.py b/test/test_state.py
index a01feb9..16b2047 100644
--- a/test/test_state.py
+++ b/test/test_state.py
@@ -1,4 +1,4 @@
-"""Test for VehicleState."""
+"""Test for old deprecate state api."""
 
 import unittest
 from unittest import mock
@@ -6,8 +6,9 @@ import datetime
 from test import load_response_json, TEST_REGION, TEST_PASSWORD, TEST_USERNAME, BackendMock, G31_VIN, \
     ATTRIBUTE_MAPPING, MISSING_ATTRIBUTES, F48_VIN
 from bimmer_connected.account import ConnectedDriveAccount
-from bimmer_connected.state import VehicleState, LidState, LockState, ConditionBasedServiceStatus, \
-    ParkingLightState, ChargingState
+from bimmer_connected.state import VehicleState, LockState, ParkingLightState, ChargingState
+from bimmer_connected.const import SERVICE_STATUS
+from bimmer_connected.vehicle_status import ConditionBasedServiceStatus, LidState
 
 G31_TEST_DATA = load_response_json('G31_NBTevo/status.json')
 G31_NO_POSITION_TEST_DATA = load_response_json('G31_NBTevo/status_position_disabled.json')
@@ -24,7 +25,7 @@ class TestState(unittest.TestCase):
         """Test if the parsing of the attributes is working."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = G31_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
 
         self.assertEqual(4126, state.mileage)
 
@@ -62,7 +63,7 @@ class TestState(unittest.TestCase):
         """Test parsing of G31 data with position tracking disabled in the vehicle."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = G31_NO_POSITION_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = G31_NO_POSITION_TEST_DATA['vehicleStatus']
 
         self.assertFalse(state.is_vehicle_tracking_enabled)
         self.assertIsNone(state.gps_position)
@@ -71,7 +72,7 @@ class TestState(unittest.TestCase):
         """Test if the parsing of the attributes is working."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = F48_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
 
         self.assertEqual(21529, state.mileage)
 
@@ -108,7 +109,7 @@ class TestState(unittest.TestCase):
         """Test if the parsing of the attributes is working."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = I01_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = I01_TEST_DATA['vehicleStatus']
 
         self.assertEqual(48, state.remaining_range_electric)
         self.assertEqual(154, state.remaining_range_total)
@@ -128,7 +129,7 @@ class TestState(unittest.TestCase):
         """Test if error handling is working correctly."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = dict()
+        state._attributes[SERVICE_STATUS] = dict()
         self.assertIsNone(state.mileage)
 
     @mock.patch('bimmer_connected.vehicle.VehicleState.update_data')
@@ -146,7 +147,7 @@ class TestState(unittest.TestCase):
             vehicle = account.get_vehicle(G31_VIN)
 
             vehicle.state.update_data()
-            self.assertEqual(vehicle.state._attributes, {})
+            self.assertEqual(vehicle.state._attributes[SERVICE_STATUS], {})
 
             backend_mock.setup_default_vehicles()
 
@@ -157,7 +158,7 @@ class TestState(unittest.TestCase):
         """Test features around lids."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = G31_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
 
         for lid in state.lids:
             self.assertEqual(LidState.CLOSED, lid.state)
@@ -166,30 +167,30 @@ class TestState(unittest.TestCase):
         self.assertEqual(0, len(list(state.open_lids)))
         self.assertTrue(state.all_lids_closed)
 
-        state._attributes['doorDriverFront'] = LidState.OPEN
+        state._attributes[SERVICE_STATUS]['doorDriverFront'] = LidState.OPEN
         self.assertFalse(state.all_lids_closed)
 
     def test_windows_g31(self):
         """Test features around lids."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = G31_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
 
-        for window in state.windows:
+        for window in state.vehicle_status.windows:
             self.assertEqual(LidState.CLOSED, window.state)
 
         self.assertEqual(5, len(list(state.windows)))
         self.assertEqual(0, len(list(state.open_windows)))
         self.assertTrue(state.all_windows_closed)
 
-        state._attributes['windowDriverFront'] = LidState.INTERMEDIATE
+        state._attributes[SERVICE_STATUS]['windowDriverFront'] = LidState.INTERMEDIATE
         self.assertFalse(state.all_windows_closed)
 
     def test_windows_f48(self):
         """Test features around lids."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = F48_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
 
         for window in state.windows:
             self.assertEqual(LidState.CLOSED, window.state)
@@ -200,7 +201,7 @@ class TestState(unittest.TestCase):
         """Test the door locks."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = G31_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
 
         self.assertEqual(LockState.SECURED, state.door_lock_state)
 
@@ -244,9 +245,9 @@ class TestState(unittest.TestCase):
         """Test parsing of a check control message."""
         account = unittest.mock.MagicMock(ConnectedDriveAccount)
         state = VehicleState(account, None)
-        state._attributes = F48_TEST_DATA['vehicleStatus']
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
 
-        ccms = state.check_control_messages
+        ccms = state.vehicle_status.check_control_messages
         self.assertEqual(1, len(ccms))
         ccm = ccms[0]
         self.assertEqual(955, ccm["ccmId"])
diff --git a/test/test_vehicle_status.py b/test/test_vehicle_status.py
new file mode 100644
index 0000000..1708772
--- /dev/null
+++ b/test/test_vehicle_status.py
@@ -0,0 +1,257 @@
+"""Test for VehicleState."""
+
+import unittest
+from unittest import mock
+import datetime
+from test import load_response_json, TEST_REGION, TEST_PASSWORD, TEST_USERNAME, BackendMock, G31_VIN, \
+    ATTRIBUTE_MAPPING, MISSING_ATTRIBUTES, F48_VIN
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.state import VehicleState
+from bimmer_connected.const import SERVICE_STATUS
+from bimmer_connected.vehicle_status import LidState, LockState, ConditionBasedServiceStatus, \
+    ParkingLightState, ChargingState
+
+G31_TEST_DATA = load_response_json('G31_NBTevo/status.json')
+G31_NO_POSITION_TEST_DATA = load_response_json('G31_NBTevo/status_position_disabled.json')
+F48_TEST_DATA = load_response_json('F48/status.json')
+I01_TEST_DATA = load_response_json('I01_REX/status.json')
+
+
+class TestState(unittest.TestCase):
+    """Test for VehicleState."""
+
+    # pylint: disable=protected-access
+
+    def test_parse_g31(self):
+        """Test if the parsing of the attributes is working."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
+
+        self.assertEqual(4126, state.vehicle_status.mileage)
+
+        zone = datetime.timezone(datetime.timedelta(0, 3600))
+        self.assertEqual(datetime.datetime(year=2018, month=3, day=10, hour=11, minute=39, second=41, tzinfo=zone),
+                         state.vehicle_status.timestamp)
+
+        self.assertTrue(state.vehicle_status.is_vehicle_tracking_enabled)
+        self.assertAlmostEqual(50.5050, state.vehicle_status.gps_position[0])
+        self.assertAlmostEqual(10.1010, state.vehicle_status.gps_position[1])
+
+        self.assertAlmostEqual(33, state.vehicle_status.remaining_fuel)
+
+        self.assertAlmostEqual(321, state.vehicle_status.remaining_range_fuel)
+        self.assertAlmostEqual(state.vehicle_status.remaining_range_fuel, state.vehicle_status.remaining_range_total)
+
+        self.assertEqual('VEHICLE_SHUTDOWN', state.vehicle_status.last_update_reason)
+
+        cbs = state.vehicle_status.condition_based_services
+        self.assertEqual(3, len(cbs))
+        self.assertEqual(ConditionBasedServiceStatus.OK, cbs[0].state)
+        self.assertEqual(datetime.datetime(year=2020, month=1, day=1), cbs[0].due_date)
+        self.assertEqual(25000, cbs[0].due_distance)
+
+        self.assertEqual(ConditionBasedServiceStatus.OK, cbs[1].state)
+        self.assertEqual(datetime.datetime(year=2022, month=1, day=1), cbs[1].due_date)
+        self.assertEqual(60000, cbs[1].due_distance)
+
+        self.assertTrue(state.vehicle_status.are_all_cbs_ok)
+
+        self.assertFalse(state.vehicle_status.are_parking_lights_on)
+        self.assertEqual(ParkingLightState.OFF, state.vehicle_status.parking_lights)
+
+    def test_parse_g31_no_psoition(self):
+        """Test parsing of G31 data with position tracking disabled in the vehicle."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = G31_NO_POSITION_TEST_DATA['vehicleStatus']
+
+        self.assertFalse(state.vehicle_status.is_vehicle_tracking_enabled)
+        self.assertIsNone(state.vehicle_status.gps_position)
+
+    def test_parse_f48(self):
+        """Test if the parsing of the attributes is working."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
+
+        self.assertEqual(21529, state.vehicle_status.mileage)
+
+        zone = datetime.timezone(datetime.timedelta(0, 3600))
+        self.assertEqual(datetime.datetime(year=2018, month=3, day=10, hour=19, minute=35, second=30, tzinfo=zone),
+                         state.vehicle_status.timestamp)
+
+        self.assertTrue(state.vehicle_status.is_vehicle_tracking_enabled)
+        self.assertAlmostEqual(50.505050, state.vehicle_status.gps_position[0])
+        self.assertAlmostEqual(10.1010101, state.vehicle_status.gps_position[1])
+
+        self.assertAlmostEqual(39, state.vehicle_status.remaining_fuel)
+
+        self.assertAlmostEqual(590, state.vehicle_status.remaining_range_fuel)
+
+        self.assertEqual('DOOR_STATE_CHANGED', state.vehicle_status.last_update_reason)
+
+        cbs = state.vehicle_status.condition_based_services
+        self.assertEqual(3, len(cbs))
+        self.assertEqual(ConditionBasedServiceStatus.OK, cbs[0].state)
+        self.assertEqual(datetime.datetime(year=2019, month=7, day=1), cbs[0].due_date)
+        self.assertEqual(9000, cbs[0].due_distance)
+
+        self.assertEqual(ConditionBasedServiceStatus.OK, cbs[1].state)
+        self.assertEqual(datetime.datetime(year=2021, month=7, day=1), cbs[1].due_date)
+        self.assertEqual(39000, cbs[1].due_distance)
+
+        self.assertTrue(state.vehicle_status.are_all_cbs_ok)
+
+        self.assertFalse(state.vehicle_status.are_parking_lights_on)
+        self.assertEqual(ParkingLightState.OFF, state.vehicle_status.parking_lights)
+
+    def test_parse_i01(self):
+        """Test if the parsing of the attributes is working."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = I01_TEST_DATA['vehicleStatus']
+
+        self.assertEqual(48, state.vehicle_status.remaining_range_electric)
+        self.assertEqual(154, state.vehicle_status.remaining_range_total)
+        self.assertEqual(94, state.vehicle_status.max_range_electric)
+        self.assertEqual(ChargingState.CHARGING, state.vehicle_status.charging_status)
+        self.assertEqual(datetime.timedelta(minutes=332), state.vehicle_status.charging_time_remaining)
+        self.assertEqual(54, state.vehicle_status.charging_level_hv)
+
+    def test_parse_timeformat(self):
+        """Test parsing of the time string."""
+        date = "2018-03-10T11:39:41+0100"
+        zone = datetime.timezone(datetime.timedelta(0, 3600))
+        self.assertEqual(datetime.datetime(year=2018, month=3, day=10, hour=11, minute=39, second=41, tzinfo=zone),
+                         VehicleState._parse_datetime(date))
+
+    def test_missing_attribute(self):
+        """Test if error handling is working correctly."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = dict()
+        self.assertIsNone(state.vehicle_status.mileage)
+
+    @mock.patch('bimmer_connected.vehicle.VehicleState.update_data')
+    def test_no_attributes(self, _):
+        """Test if error handling is working correctly."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        self.assertIsNone(state.vehicle_status.mileage)
+
+    def test_update_data(self):
+        """Test update_data method."""
+        backend_mock = BackendMock()
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+            vehicle = account.get_vehicle(G31_VIN)
+
+            vehicle.state.update_data()
+            self.assertEqual(vehicle.state._attributes[SERVICE_STATUS], {})
+
+            backend_mock.setup_default_vehicles()
+
+            vehicle.state.update_data()
+            self.assertEqual(4126, vehicle.state.vehicle_status.mileage)
+
+    def test_lids(self):
+        """Test features around lids."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
+
+        for lid in state.vehicle_status.lids:
+            self.assertEqual(LidState.CLOSED, lid.state)
+
+        self.assertEqual(6, len(list(state.vehicle_status.lids)))
+        self.assertEqual(0, len(list(state.vehicle_status.open_lids)))
+        self.assertTrue(state.vehicle_status.all_lids_closed)
+
+        state._attributes[SERVICE_STATUS]['doorDriverFront'] = LidState.OPEN
+        self.assertFalse(state.vehicle_status.all_lids_closed)
+
+    def test_windows_g31(self):
+        """Test features around lids."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
+
+        for window in state.vehicle_status.windows:
+            self.assertEqual(LidState.CLOSED, window.state)
+
+        self.assertEqual(5, len(list(state.vehicle_status.windows)))
+        self.assertEqual(0, len(list(state.vehicle_status.open_windows)))
+        self.assertTrue(state.vehicle_status.all_windows_closed)
+
+        state._attributes[SERVICE_STATUS]['windowDriverFront'] = LidState.INTERMEDIATE
+        self.assertFalse(state.vehicle_status.all_windows_closed)
+
+    def test_windows_f48(self):
+        """Test features around lids."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
+
+        for window in state.vehicle_status.windows:
+            self.assertEqual(LidState.CLOSED, window.state)
+
+        self.assertEqual(4, len(list(state.vehicle_status.windows)))
+
+    def test_door_locks(self):
+        """Test the door locks."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = G31_TEST_DATA['vehicleStatus']
+
+        self.assertEqual(LockState.SECURED, state.vehicle_status.door_lock_state)
+
+    def test_parsing_attributes(self):
+        """Test parsing different attributes of the vehicle.
+
+        Just make sure parsing that no exception is raised and we get not-None values.
+        """
+        backend_mock = BackendMock()
+        # list of attributes that are ignored at the moment
+        ignored_attributes = [ATTRIBUTE_MAPPING.get(a, a) for a in MISSING_ATTRIBUTES]
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            backend_mock.setup_default_vehicles()
+            account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+            account.update_vehicle_states()
+
+            for vehicle in account.vehicles:
+                print(vehicle.name)
+                for attribute in (a for a in vehicle.available_attributes if a not in ignored_attributes):
+                    self.assertIsNotNone(getattr(vehicle.state.vehicle_status, attribute), attribute)
+
+    def test_check_control_messages(self):
+        """Test handling of check control messages.
+
+        F48 is the only vehicle with active Check Control Messages, so we only expect to get something there.
+        """
+        backend_mock = BackendMock()
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            backend_mock.setup_default_vehicles()
+            account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
+            account.update_vehicle_states()
+
+            for vehicle in account.vehicles:
+                print(vehicle.name, vehicle.vin)
+                if vehicle.vin == F48_VIN:
+                    self.assertTrue(vehicle.state.vehicle_status.has_check_control_messages)
+                else:
+                    self.assertFalse(vehicle.state.vehicle_status.has_check_control_messages)
+
+    def test_ccm_f48(self):
+        """Test parsing of a check control message."""
+        account = unittest.mock.MagicMock(ConnectedDriveAccount)
+        state = VehicleState(account, None)
+        state._attributes[SERVICE_STATUS] = F48_TEST_DATA['vehicleStatus']
+
+        ccms = state.vehicle_status.check_control_messages
+        self.assertEqual(1, len(ccms))
+        ccm = ccms[0]
+        self.assertEqual(955, ccm["ccmId"])
+        self.assertEqual(41544, ccm["ccmMileage"])
+        self.assertIn("Tyre pressure", ccm["ccmDescriptionShort"])
+        self.assertIn("continue driving", ccm["ccmDescriptionLong"])