Repository: bimmerconnected/bimmer_connected
Pull request #312 (2021-08-14 10:15:15)

Fix remote services in rest_of_world and north_america
## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
Merge Fix remote services in rest_of_world (#310)


## Type of change
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (which adds functionality to this library)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [ ] The code change is tested and works locally.
- [ ] Tests have been added to verify that the new code works.


rikroe: @gerard33, @pzim-devdata could you both please test against `dev` if remote services are working fully for you?
@heyepic, @dazole could you guys please test if remote execution in north america works as well? Can't test it by myself, as I don't have any cars in my account there ;)
dazole: I was able successfully test status, light flash, fingerprint vehicle finder and send message. "vehicle finder" only returned "success", though, and not any kind of lat/lon or anything else.  I'm not sure if that's expected or not.

I don't use HA, though, so I wasn't able to test door lock or ventilation.
rikroe: > I was able successfully test status, light flash, fingerprint vehicle finder and send message. "vehicle finder" only returned "success", though, and not any kind of lat/lon or anything else. I'm not sure if that's expected or not.

Thanks, that's great to hear! `vehicle_finder` doesn't give a result, but the timestamp in status will be updated.

Patch:
diff --git a/.gitignore b/.gitignore
index b579521..78b1f3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@ dist/
 AUTHORS
 ChangeLog
 /.eggs
-build/
\ No newline at end of file
+build/
+htmlcov/
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 307b5c9..10f35ab 100644
--- a/README.rst
+++ b/README.rst
@@ -117,6 +117,7 @@ to be used in Home Assistant and is available on pypi.
 * https://github.com/edent/BMW-i-Remote
 * https://github.com/jupe76/bmwcdapi
 * https://github.com/frankjoke/iobroker.bmw
+* https://github.com/TA2k/ioBroker.bmw
 
 Thank you for your great software!
 
diff --git a/bimmer_connected/account.py b/bimmer_connected/account.py
index 7ca8205..1f1d264 100644
--- a/bimmer_connected/account.py
+++ b/bimmer_connected/account.py
@@ -20,7 +20,8 @@ from requests.exceptions import HTTPError
 
 from bimmer_connected.country_selector import (
     Regions,
-    get_server_url,
+    get_server_url_legacy,
+    get_server_url_eadrax,
     get_gcdm_oauth_endpoint,
     get_gcdm_oauth_authorization
 )
@@ -51,7 +52,8 @@ class ConnectedDriveAccount:  # pylint: disable=too-many-instance-attributes
     def __init__(self, username: str, password: str, region: Regions, log_responses: pathlib.Path = None,
                  retries_on_500_error: int = 5) -> None:
         self._region = region
-        self._server_url = None
+        self._server_url_legacy = None
+        self._server_url_eadrax = None
         self._username = username
         self._password = password
         self._oauth_token = None
@@ -178,7 +180,6 @@ class ConnectedDriveAccount:  # pylint: disable=too-many-instance-attributes
         headers = {
             "accept": "application/json",
             "Authorization": "Bearer {}".format(self._oauth_token),
-            "referer": "https://www.bmw-connecteddrive.de/app/index.html",
         }
         return headers
 
@@ -216,6 +217,37 @@ class ConnectedDriveAccount:  # pylint: disable=too-many-instance-attributes
         self._log_response_to_file(response, logfilename)
         return response
 
+    def send_request_v2(self, url: str, data=None, headers=None, post=False, allow_redirects=True,
+                        logfilename: str = None, params: dict = None):
+        """Send an http request to the server.
+
+        If the http headers are not set, default headers are generated.
+        You can choose if you want a GET or POST request.
+        """
+        self._get_oauth_token()
+        request_headers = {
+            "x-user-agent": "android(v1.07_20200330);bmw;1.5.2(8932)",
+            "Authorization": "Bearer {}".format(self._oauth_token),
+        }
+        if headers:
+            request_headers.update(headers)
+
+        if post:
+            response = requests.post(url, headers=request_headers, data=data, allow_redirects=allow_redirects,
+                                     params=params)
+        else:
+            response = requests.get(url, headers=request_headers, data=data, allow_redirects=allow_redirects,
+                                    params=params)
+
+        try:
+            response.raise_for_status()
+        except HTTPError as ex:
+            _LOGGER.exception(ex)
+            raise ex
+
+        self._log_response_to_file(response, logfilename)
+        return response
+
     def _log_response_to_file(self, response: requests.Response, logfilename: str = None) -> None:
         """If a log path is set, log all resonses to a file."""
         if self._log_responses is None or logfilename is None:
@@ -263,17 +295,29 @@ class ConnectedDriveAccount:  # pylint: disable=too-many-instance-attributes
         return json_data
 
     @property
-    def server_url(self) -> str:
+    def server_url_legacy(self) -> str:
+        """Get the url of the server for this country."""
+        if self._server_url_legacy is None:
+            self._server_url_legacy = get_server_url_legacy(self._region)
+        return self._server_url_legacy
+
+    @property
+    def server_url_eadrax(self) -> str:
         """Get the url of the server for this country."""
-        if self._server_url is None:
-            self._server_url = get_server_url(self._region)
-        return self._server_url
+        if self._server_url_eadrax is None:
+            self._server_url_eadrax = get_server_url_eadrax(self._region)
+        return self._server_url_eadrax
+
+    @property
+    def region(self) -> str:
+        """Get the region."""
+        return self._region
 
     def _get_vehicles(self):
         """Retrieve list of vehicle for the account."""
         _LOGGER.debug('Getting vehicle list')
         self._get_oauth_token()
-        response = self.send_request(VEHICLES_URL.format(server=self.server_url), headers=self.request_header,
+        response = self.send_request(VEHICLES_URL.format(server=self.server_url_legacy), headers=self.request_header,
                                      logfilename='vehicles')
 
         for vehicle_dict in response.json()['vehicles']:
diff --git a/bimmer_connected/const.py b/bimmer_connected/const.py
index 589c712..c72a242 100644
--- a/bimmer_connected/const.py
+++ b/bimmer_connected/const.py
@@ -8,10 +8,17 @@ BASE_URL_LEGACY = 'https://{server}/api/vehicle'
 VEHICLES_URL = BASE_URL + '/user/vehicles'
 VEHICLE_VIN_URL = VEHICLES_URL + '/{vin}'
 VEHICLE_STATUS_URL = VEHICLE_VIN_URL + '/status'
+
 REMOTE_SERVICE_STATUS_URL = VEHICLE_VIN_URL + '/serviceExecutionStatus?serviceType={service_type}'
 REMOTE_SERVICE_URL = VEHICLE_VIN_URL + "/executeService"
+
+REMOTE_SERVICE_EADRAX_BASE_URL = 'https://{server}/eadrax-vrccs/v2/presentation/remote-commands'
+REMOTE_SERVICE_EADRAX_URL = REMOTE_SERVICE_EADRAX_BASE_URL + '/{vin}/{service_type}'
+REMOTE_SERVICE_EADRAX_STATUS_URL = REMOTE_SERVICE_EADRAX_BASE_URL + '/eventStatus?eventId={event_id}'
+
 VEHICLE_IMAGE_URL = VEHICLE_VIN_URL + "/image?width={width}&height={height}&view={view}"
 VEHICLE_POI_URL = VEHICLE_VIN_URL + '/sendpoi'
+VEHICLE_EADRAX_POI_URL = 'https://{server}/eadrax-dcs/v1/send-to-car/send-to-car'
 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'
diff --git a/bimmer_connected/country_selector.py b/bimmer_connected/country_selector.py
index cfe8186..6d0f706 100644
--- a/bimmer_connected/country_selector.py
+++ b/bimmer_connected/country_selector.py
@@ -14,12 +14,19 @@ class Regions(Enum):
 
 
 #: Mapping from regions to servers
-_SERVER_URLS = {
+_SERVER_URLS_LEGACY = {
     Regions.NORTH_AMERICA: "b2vapi.bmwgroup.us",
     Regions.REST_OF_WORLD: "b2vapi.bmwgroup.com",
     Regions.CHINA: "b2vapi.bmwgroup.cn:8592",
 }
 
+_SERVER_URLS_EADRAX = {
+    Regions.NORTH_AMERICA: "cocoapi.bmwgroup.us",
+    Regions.REST_OF_WORLD: "cocoapi.bmwgroup.com",
+    Regions.CHINA: None,
+}
+
+
 #: Mapping from regions to servers
 _GCDM_OAUTH_ENDPOINTS = {
     Regions.NORTH_AMERICA: "login.bmwusa.com/gcdm",
@@ -84,9 +91,14 @@ def get_region_from_name(name: str) -> Regions:
     )
 
 
-def get_server_url(region: Regions) -> str:
+def get_server_url_legacy(region: Regions) -> str:
+    """Get the url of the server for the region."""
+    return _SERVER_URLS_LEGACY[region]
+
+
+def get_server_url_eadrax(region: Regions) -> str:
     """Get the url of the server for the region."""
-    return _SERVER_URLS[region]
+    return _SERVER_URLS_EADRAX[region]
 
 
 def get_gcdm_oauth_endpoint(region: Regions) -> str:
diff --git a/bimmer_connected/remote_services.py b/bimmer_connected/remote_services.py
index 67abc91..d039553 100644
--- a/bimmer_connected/remote_services.py
+++ b/bimmer_connected/remote_services.py
@@ -2,6 +2,7 @@
 
 import datetime
 import json
+from json.decoder import JSONDecodeError
 import logging
 import time
 from typing import List
@@ -12,17 +13,20 @@ import requests
 
 from bimmer_connected.const import (REMOTE_SERVICE_STATUS_URL,
                                     REMOTE_SERVICE_URL,
-                                    VEHICLE_POI_URL)
+                                    REMOTE_SERVICE_EADRAX_STATUS_URL,
+                                    REMOTE_SERVICE_EADRAX_URL,
+                                    VEHICLE_POI_URL,
+                                    VEHICLE_EADRAX_POI_URL)
 
 TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
 
 _LOGGER = logging.getLogger(__name__)
 
 #: time in seconds between polling updates on the status of a remote service
-_POLLING_CYCLE = 1
+_POLLING_CYCLE = 3
 
 #: maximum number of seconds to wait for the server to return a positive answer
-_POLLING_TIMEOUT = 120
+_POLLING_TIMEOUT = 240
 
 #: time in seconds to wait before updating the vehicle state from the server
 _UPDATE_AFTER_REMOTE_SERVICE_DELAY = 10
@@ -117,12 +121,13 @@ class RemoteServiceStatus:  # pylint: disable=too-few-public-methods
 
     def __init__(self, response: dict):
         """Construct a new object from a dict."""
-        status = response.get('executionStatus', {})
-        if len(status) == 0:
-            _LOGGER.warning("executionStatus not in response: %s", response)
+        status = None
+        if 'executionStatus' in response:
+            status = response["executionStatus"].get('status')
+        elif 'eventStatus' in response:
+            status = response.get("eventStatus")
 
-        self.state = ExecutionState(status.get('status', 'UNKNOWN'))
-        self.event_id = status.get('eventId')
+        self.state = ExecutionState(status or 'UNKNOWN')
 
     @staticmethod
     def _parse_timestamp(timestamp: str) -> datetime.datetime:
@@ -148,9 +153,8 @@ class RemoteServices:
         A state update is NOT triggered after this, as the vehicle state is unchanged.
         """
         _LOGGER.debug('Triggering remote light flash')
-        # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_LIGHT_FLASH, post=True)
-        return self._block_until_done(_Services.REMOTE_LIGHT_FLASH)
+        event_id = self._trigger_remote_service(_Services.REMOTE_LIGHT_FLASH)
+        return self._block_until_done(_Services.REMOTE_LIGHT_FLASH, event_id)
 
     def trigger_remote_door_lock(self) -> RemoteServiceStatus:
         """Trigger the vehicle to lock its doors.
@@ -158,9 +162,8 @@ class RemoteServices:
         A state update is triggered after this, as the lock state of the vehicle changes.
         """
         _LOGGER.debug('Triggering remote door lock')
-        # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_DOOR_LOCK, post=True)
-        result = self._block_until_done(_Services.REMOTE_DOOR_LOCK)
+        event_id = self._trigger_remote_service(_Services.REMOTE_DOOR_LOCK)
+        result = self._block_until_done(_Services.REMOTE_DOOR_LOCK, event_id)
         self._trigger_state_update()
         return result
 
@@ -170,9 +173,8 @@ class RemoteServices:
         A state update is triggered after this, as the lock state of the vehicle changes.
         """
         _LOGGER.debug('Triggering remote door unlock')
-        # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_DOOR_UNLOCK, post=True)
-        result = self._block_until_done(_Services.REMOTE_DOOR_UNLOCK)
+        event_id = self._trigger_remote_service(_Services.REMOTE_DOOR_UNLOCK)
+        result = self._block_until_done(_Services.REMOTE_DOOR_UNLOCK, event_id)
         self._trigger_state_update()
         return result
 
@@ -182,9 +184,8 @@ class RemoteServices:
         A state update is NOT triggered after this, as the vehicle state is unchanged.
         """
         _LOGGER.debug('Triggering remote horn sound')
-        # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_HORN, post=True)
-        return self._block_until_done(_Services.REMOTE_HORN)
+        event_id = self._trigger_remote_service(_Services.REMOTE_HORN)
+        return self._block_until_done(_Services.REMOTE_HORN, event_id)
 
     def trigger_remote_air_conditioning(self) -> RemoteServiceStatus:
         """Trigger the air conditioning to start.
@@ -192,50 +193,86 @@ class RemoteServices:
         A state update is NOT triggered after this, as the vehicle state is unchanged.
         """
         _LOGGER.debug('Triggering remote air conditioning')
-        # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_AIR_CONDITIONING, post=True)
-        result = self._block_until_done(_Services.REMOTE_AIR_CONDITIONING)
+        event_id = self._trigger_remote_service(_Services.REMOTE_AIR_CONDITIONING, action="START")
+        result = self._block_until_done(_Services.REMOTE_AIR_CONDITIONING, event_id)
+        self._trigger_state_update()
+        return result
+
+    def trigger_remote_air_conditioning_stop(self) -> RemoteServiceStatus:
+        """Trigger the air conditioning to stop.
+
+        A state update is NOT triggered after this, as the vehicle state is unchanged.
+        """
+        _LOGGER.debug('Triggering remote air conditioning')
+        event_id = self._trigger_remote_service(_Services.REMOTE_AIR_CONDITIONING, action="STOP")
+        result = self._block_until_done(_Services.REMOTE_AIR_CONDITIONING, event_id)
         self._trigger_state_update()
         return result
 
-    def _trigger_remote_service(self, service_id: _Services, post=False) -> requests.Response:
+    def _trigger_remote_service(self, service_id: _Services, action=None, post=True) -> requests.Response:
         """Trigger a generic remote service.
 
         You can choose if you want a POST or a GET operation.
         """
-        data = {'serviceType': service_id.value}
-        url = REMOTE_SERVICE_URL.format(vin=self._vehicle.vin, server=self._account.server_url)
-
-        return self._account.send_request(url, post=post, data=data)
+        if self._account.server_url_eadrax:
+            url = REMOTE_SERVICE_EADRAX_URL.format(
+                server=self._account.server_url_eadrax,
+                vin=self._vehicle.vin,
+                service_type=service_id.value.lower().replace('_', '-')
+            )
+            params = {"action": action}
+            response = self._account.send_request_v2(url, post=post, params=params)
+        else:
+            data = {'serviceType': service_id.value}
+            url = REMOTE_SERVICE_URL.format(vin=self._vehicle.vin, server=self._account.server_url_legacy)
+            response = self._account.send_request(url, post=post, data=data)
+        try:
+            return response.json().get('eventId')
+        except JSONDecodeError:
+            return None
 
-    def _block_until_done(self, service: _Services) -> RemoteServiceStatus:
+    def _block_until_done(self, service: _Services = None, event_id: str = None) -> RemoteServiceStatus:
         """Keep polling the server until we get a final answer.
 
         :raises IOError: if there is no final answer before _POLLING_TIMEOUT
         """
+        if not service and not event_id:
+            raise ValueError("One of 'service' or 'event_id' is required.")
         fail_after = datetime.datetime.now() + datetime.timedelta(seconds=_POLLING_TIMEOUT)
         while True:
-            status = self._get_remote_service_status(service)
+            status = self._get_remote_service_status(service, event_id)
             _LOGGER.debug('current state if remote service is: %s', status.state.value)
             if status.state not in [ExecutionState.UNKNOWN, ExecutionState.PENDING, ExecutionState.DELIVERED]:
                 return status
             if datetime.datetime.now() > fail_after:
-                raise IOError(
-                    'Timeout on getting final answer from server. Current state: {}'.format(status.state.value))
+                raise TimeoutError(
+                    'Did not receive remote service result in {} seconds. Current state: {}'.format(
+                        _POLLING_TIMEOUT,
+                        status.state.value
+                    ))
             time.sleep(_POLLING_CYCLE)
 
-    def _get_remote_service_status(self, service: _Services) -> RemoteServiceStatus:
+    def _get_remote_service_status(self, service: _Services = None, event_id: str = None) -> RemoteServiceStatus:
         """The the execution status of the last remote service that was triggered.
 
         As the status changes over time, you probably need to poll this.
         Recommended polling time is AT LEAST one second as the reaction is sometimes quite slow.
         """
+        if not service and not event_id:
+            raise ValueError("One of 'service' or 'event_id' is required.")
         _LOGGER.debug('getting remote service status')
-        url = REMOTE_SERVICE_STATUS_URL.format(
-            server=self._account.server_url,
-            vin=self._vehicle.vin,
-            service_type=service.value)
-        response = self._account.send_request(url)
+        if self._account.server_url_eadrax:
+            url = REMOTE_SERVICE_EADRAX_STATUS_URL.format(
+                server=self._account.server_url_eadrax,
+                vin=self._vehicle.vin,
+                event_id=event_id)
+            response = self._account.send_request_v2(url, post=True)
+        elif service:
+            url = REMOTE_SERVICE_STATUS_URL.format(
+                server=self._account.server_url_legacy,
+                vin=self._vehicle.vin,
+                service_type=service.value)
+            response = self._account.send_request(url)
         try:
             json_result = response.json()
             return RemoteServiceStatus(json_result)
@@ -278,15 +315,42 @@ class RemoteServices:
         if "lat" not in data or "lon" not in data:
             raise TypeError("__init__() missing 2 required positional arguments: 'lat' and 'lon'")
         poi = PointOfInterest(**data)
-        self._send_message(Message.from_poi(poi))
+        if self._account.server_url_eadrax:
+            data_json = json.dumps({
+                "location": {
+                    "coordinates": {
+                        "latitude": data["lat"],
+                        "longitude": data["lon"]
+                    },
+                    "name": data.get('name'),
+                    "locationAddress": {
+                        "street": data.get('street'),
+                        "postalCode": data.get('postal_code'),
+                        "city": data.get('city'),
+                        "country": data.get('country'),
+                    },
+                    "type": "SHARED_DESTINATION_FROM_EXTERNAL_APP"
+                },
+                "vin": self._vehicle.vin
+            })
+            self._account.send_request_v2(
+                VEHICLE_EADRAX_POI_URL.format(server=self._account.server_url_eadrax),
+                post=True,
+                headers={"Content-Type": "application/json"},
+                data=data_json
+            )
+        else:
+            self._send_message(Message.from_poi(poi))
         # _send_message has no separate ExecutionStates
-        return RemoteServiceStatus({'executionStatus': {'status': 'EXECUTED', 'eventId': -1}})
+        return RemoteServiceStatus({'eventStatus': 'EXECUTED'})
 
     def _send_message(self, msg: Message) -> None:
         """Send a message/point of interest to the vehicle."""
+        if self._account.server_url_eadrax:
+            raise NotImplementedError('The My BMW API does not support sending messages without location.')
         url = VEHICLE_POI_URL.format(
             vin=self._vehicle.vin,
-            server=self._account.server_url
+            server=self._account.server_url_legacy
         )
         header = self._account.request_header
         # the accept field of the header needs to be updated not the usual JSON
@@ -304,7 +368,7 @@ class RemoteServices:
         """
         _LOGGER.debug('Triggering remote vehicle finder')
         # needs to be called via POST, GET is not working
-        self._trigger_remote_service(_Services.REMOTE_VEHICLE_FINDER, post=True)
-        result = self._block_until_done(_Services.REMOTE_VEHICLE_FINDER)
+        event_id = self._trigger_remote_service(_Services.REMOTE_VEHICLE_FINDER)
+        result = self._block_until_done(_Services.REMOTE_VEHICLE_FINDER, event_id)
         self._trigger_state_update()
         return result
diff --git a/bimmer_connected/state.py b/bimmer_connected/state.py
index 0d1bb3a..ada0cdb 100644
--- a/bimmer_connected/state.py
+++ b/bimmer_connected/state.py
@@ -2,7 +2,7 @@
 
 import logging
 import datetime
-from typing import List
+from typing import List, Tuple
 
 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, \
@@ -97,7 +97,7 @@ class VehicleState:
         for service in self._vehicle.available_state_services:
             try:
                 response = self._account.send_request(
-                    self._url[service].format(server=self._account.server_url, vin=self._vehicle.vin),
+                    self._url[service].format(server=self._account.server_url_legacy, vin=self._vehicle.vin),
                     logfilename=service, params=params)
                 if not self._key[service]:
                     self._attributes[service] = response.json()
@@ -140,7 +140,7 @@ class VehicleState:
 
     @property
     @backend_parameter
-    def gps_position(self) -> (float, float):
+    def gps_position(self) -> Tuple[float, float]:
         """DEPRECATED: Use state.vehicle_status.gps_position instead.
 
         Get the last known position of the vehicle.
diff --git a/bimmer_connected/vehicle.py b/bimmer_connected/vehicle.py
index feaa725..a82d785 100644
--- a/bimmer_connected/vehicle.py
+++ b/bimmer_connected/vehicle.py
@@ -216,7 +216,7 @@ class ConnectedDriveVehicle:
         """
         url = VEHICLE_IMAGE_URL.format(
             vin=self.vin,
-            server=self._account.server_url,
+            server=self._account.server_url_legacy,
             width=width,
             height=height,
             view=direction.value,


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index f15ff9b..fbf03d5 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -6,6 +6,8 @@ import re
 import urllib
 from typing import List
 
+from requests.exceptions import HTTPError
+
 from bimmer_connected.country_selector import Regions
 
 RESPONSE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'responses')
@@ -216,11 +218,10 @@ class MockResponse:
     # pylint: disable=too-many-arguments
 
     def __init__(self, regex: str, data: str = None, data_files: List[str] = None, headers: dict = None,
-                 status_code: int = 200, raise_for_status=None) -> None:
+                 status_code: int = 200) -> None:
         """Constructor."""
         self.regex = re.compile(regex)
         self.status_code = status_code
-        self.raise_for_status_side_effect = raise_for_status
         self.headers = headers
         self._usage_count = 0
         if self.headers is None:
@@ -239,9 +240,14 @@ class MockResponse:
         return json.loads(self.text)
 
     def raise_for_status(self) -> bool:
-        """Simulate requests' raise_for_status and raise HTTPError if set as sideeffect."""
-        if self.raise_for_status_side_effect:
-            raise self.raise_for_status_side_effect
+        """Simulate requests' raise_for_status and raise HTTPError if non-ok status code."""
+        http_error_msg = ''
+        if 400 <= self.status_code < 500:
+            http_error_msg = u'%s Client Error' % (self.status_code)
+        elif 500 <= self.status_code < 600:
+            http_error_msg = u'%s Server Error' % (self.status_code)
+        if http_error_msg:
+            raise HTTPError(http_error_msg, response=self)
 
     @property
     def text(self) -> str:
diff --git a/test/responses/auth/auth_error_internal_error.txt b/test/responses/auth/auth_error_internal_error.txt
new file mode 100644
index 0000000..1553933
--- /dev/null
+++ b/test/responses/auth/auth_error_internal_error.txt
@@ -0,0 +1,1 @@
+INTERNAL SERVER ERROR
\ No newline at end of file
diff --git a/test/responses/auth/auth_error_wrong_password.json b/test/responses/auth/auth_error_wrong_password.json
new file mode 100644
index 0000000..a81d310
--- /dev/null
+++ b/test/responses/auth/auth_error_wrong_password.json
@@ -0,0 +1,4 @@
+{
+    "error": "invalid_client",
+    "error_description": "Client authentication failed (e.g., login failure, unknown client, no client authentication included or unsupported authentication method)"
+}
\ No newline at end of file
diff --git a/test/responses/remote_services/eadrax_service_delivered.json b/test/responses/remote_services/eadrax_service_delivered.json
new file mode 100644
index 0000000..dc879cb
--- /dev/null
+++ b/test/responses/remote_services/eadrax_service_delivered.json
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "DELIVERED"
+}
diff --git a/test/responses/remote_services/eadrax_service_executed.json b/test/responses/remote_services/eadrax_service_executed.json
new file mode 100644
index 0000000..c2bd029
--- /dev/null
+++ b/test/responses/remote_services/eadrax_service_executed.json
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "EXECUTED"
+}
diff --git a/test/responses/remote_services/eadrax_service_initiated.json b/test/responses/remote_services/eadrax_service_initiated.json
new file mode 100644
index 0000000..6aa071e
--- /dev/null
+++ b/test/responses/remote_services/eadrax_service_initiated.json
@@ -0,0 +1,4 @@
+{
+    "creationTime": "2021-08-14T20:58:36.403Z",
+    "eventId": "009b794b-70e9-457f-a363-2e82382d300d"
+}
\ No newline at end of file
diff --git a/test/responses/remote_services/eadrax_service_pending.json b/test/responses/remote_services/eadrax_service_pending.json
new file mode 100644
index 0000000..e4df13c
--- /dev/null
+++ b/test/responses/remote_services/eadrax_service_pending.json
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "PENDING"
+}
diff --git a/test/responses/G31_NBTevo/flash_delivered.json b/test/responses/remote_services/legacy_flash_delivered.json
similarity index 100%
rename from test/responses/G31_NBTevo/flash_delivered.json
rename to test/responses/remote_services/legacy_flash_delivered.json
diff --git a/test/responses/G31_NBTevo/flash_executed.json b/test/responses/remote_services/legacy_flash_executed.json
similarity index 100%
rename from test/responses/G31_NBTevo/flash_executed.json
rename to test/responses/remote_services/legacy_flash_executed.json
diff --git a/test/responses/G31_NBTevo/flash_initiated.json b/test/responses/remote_services/legacy_flash_initiated.json
similarity index 100%
rename from test/responses/G31_NBTevo/flash_initiated.json
rename to test/responses/remote_services/legacy_flash_initiated.json
diff --git a/test/responses/G31_NBTevo/flash_pending.json b/test/responses/remote_services/legacy_flash_pending.json
similarity index 100%
rename from test/responses/G31_NBTevo/flash_pending.json
rename to test/responses/remote_services/legacy_flash_pending.json
diff --git a/test/responses/G31_NBTevo/flash_unknown.json b/test/responses/remote_services/legacy_flash_unknown.json
similarity index 100%
rename from test/responses/G31_NBTevo/flash_unknown.json
rename to test/responses/remote_services/legacy_flash_unknown.json
diff --git a/test/responses/G31_NBTevo/msg_executed.json b/test/responses/remote_services/legacy_msg_executed.json
similarity index 100%
rename from test/responses/G31_NBTevo/msg_executed.json
rename to test/responses/remote_services/legacy_msg_executed.json
diff --git a/test/test_account.py b/test/test_account.py
index e060b51..0a531cc 100644
--- a/test/test_account.py
+++ b/test/test_account.py
@@ -1,8 +1,11 @@
 """Tests for ConnectedDriveAccount."""
 import json
 import unittest
+from test import BackendMock, MockResponse, G31_VIN, TEST_PASSWORD, TEST_REGION, TEST_USERNAME
 from unittest import mock
-from test import BackendMock, G31_VIN, TEST_USERNAME, TEST_PASSWORD, TEST_REGION
+
+from requests.exceptions import HTTPError
+
 from bimmer_connected.account import ConnectedDriveAccount
 from bimmer_connected.country_selector import Regions
 
@@ -24,6 +27,29 @@ class TestAccount(unittest.TestCase):
 
             self.assertIsNone(account.get_vehicle('invalid_vin'))
 
+    def test_invalid_password(self):
+        """Test parsing the results of an invalid request"""
+        backend_mock = BackendMock()
+        backend_mock.responses = [
+            MockResponse(
+                'https://.+/gcdm/.*/?oauth/authenticate',
+                data_files=['auth/auth_error_wrong_password.json'],
+                status_code=401)
+        ]
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            with self.assertRaises(HTTPError):
+                ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.REST_OF_WORLD)
+
+        backend_mock.responses = [
+            MockResponse(
+                'https://.+/gcdm/.*/?oauth/authenticate',
+                data_files=['auth/auth_error_internal_error.txt'],
+                status_code=500)
+        ]
+        with mock.patch('bimmer_connected.account.requests', new=backend_mock):
+            with self.assertRaises(HTTPError):
+                ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.REST_OF_WORLD)
+
     def test_invalid_send_response(self):
         """Test parsing the results of an invalid request"""
         backend_mock = BackendMock()
diff --git a/test/test_remote_services.py b/test/test_remote_services.py
index 23696f8..bfce846 100644
--- a/test/test_remote_services.py
+++ b/test/test_remote_services.py
@@ -1,23 +1,33 @@
 """Test for remote_services."""
-import unittest
-from unittest import mock
 import datetime
-from test import BackendMock, TEST_USERNAME, TEST_PASSWORD, TEST_REGION, G31_VIN, load_response_json, \
-    POI_DATA, POI_REQUEST, MESSAGE_DATA, MESSAGE_REQUEST
+from test import (G31_VIN, MESSAGE_DATA, MESSAGE_REQUEST, POI_DATA,
+                  POI_REQUEST, TEST_PASSWORD, TEST_REGION, TEST_USERNAME,
+                  BackendMock, load_response_json)
+from unittest import mock, TestCase
+
+from requests.exceptions import HTTPError
 
-from bimmer_connected.account import ConnectedDriveAccount
-from bimmer_connected.remote_services import RemoteServiceStatus, ExecutionState, PointOfInterest, Message
 from bimmer_connected import remote_services
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.remote_services import (ExecutionState, Message,
+                                              PointOfInterest,
+                                              RemoteServiceStatus)
+
 
-_RESPONSE_UNKNOWN = 'G31_NBTevo/flash_unknown.json'
-_RESPONSE_INITIATED = 'G31_NBTevo/flash_initiated.json'
-_RESPONSE_PENDING = 'G31_NBTevo/flash_pending.json'
-_RESPONSE_DELIVERED = 'G31_NBTevo/flash_delivered.json'
-_RESPONSE_EXECUTED = 'G31_NBTevo/flash_executed.json'
-_MSG_EXECUTED = 'G31_NBTevo/msg_executed.json'
+_RESPONSE_LEGACY_UNKNOWN = 'remote_services/legacy_flash_unknown.json'
+_RESPONSE_LEGACY_INITIATED = 'remote_services/legacy_flash_initiated.json'
+_RESPONSE_LEGACY_PENDING = 'remote_services/legacy_flash_pending.json'
+_RESPONSE_LEGACY_DELIVERED = 'remote_services/legacy_flash_delivered.json'
+_RESPONSE_LEGACY_EXECUTED = 'remote_services/legacy_flash_executed.json'
+_MSG_EXECUTED = 'remote_services/legacy_msg_executed.json'
 
+_RESPONSE_EADRAX_INITIATED = 'remote_services/eadrax_service_initiated.json'
+_RESPONSE_EADRAX_PENDING = 'remote_services/eadrax_service_pending.json'
+_RESPONSE_EADRAX_DELIVERED = 'remote_services/eadrax_service_delivered.json'
+_RESPONSE_EADRAX_EXECUTED = 'remote_services/eadrax_service_executed.json'
 
-class TestRemoteServices(unittest.TestCase):
+
+class TestRemoteServices(TestCase):
     """Test for remote_services."""
 
     # pylint: disable=protected-access
@@ -30,19 +40,19 @@ class TestRemoteServices(unittest.TestCase):
 
     def test_states(self):
         """Test parsing the different response types."""
-        rss = RemoteServiceStatus(load_response_json(_RESPONSE_UNKNOWN))
+        rss = RemoteServiceStatus(load_response_json(_RESPONSE_LEGACY_UNKNOWN))
         self.assertEqual(ExecutionState.UNKNOWN, rss.state)
 
-        rss = RemoteServiceStatus(load_response_json(_RESPONSE_INITIATED))
+        rss = RemoteServiceStatus(load_response_json(_RESPONSE_LEGACY_INITIATED))
         self.assertEqual(ExecutionState.INITIATED, rss.state)
 
-        rss = RemoteServiceStatus(load_response_json(_RESPONSE_PENDING))
+        rss = RemoteServiceStatus(load_response_json(_RESPONSE_LEGACY_PENDING))
         self.assertEqual(ExecutionState.PENDING, rss.state)
 
-        rss = RemoteServiceStatus(load_response_json(_RESPONSE_DELIVERED))
+        rss = RemoteServiceStatus(load_response_json(_RESPONSE_LEGACY_DELIVERED))
         self.assertEqual(ExecutionState.DELIVERED, rss.state)
 
-        rss = RemoteServiceStatus(load_response_json(_RESPONSE_EXECUTED))
+        rss = RemoteServiceStatus(load_response_json(_RESPONSE_LEGACY_EXECUTED))
         self.assertEqual(ExecutionState.EXECUTED, rss.state)
 
     def test_trigger_remote_services(self):
@@ -65,8 +75,19 @@ class TestRemoteServices(unittest.TestCase):
             backend_mock = BackendMock()
             backend_mock.setup_default_vehicles()
 
-            backend_mock.add_response('https://.+/webapi/v1/user/vehicles/{vin}/executeService'.format(vin=G31_VIN),
-                                      data_files=[_RESPONSE_INITIATED])
+            backend_mock.add_response(
+                r'https://.+/eadrax-vrccs/v2/presentation/remote-commands/{vin}/.+$'.format(vin=G31_VIN),
+                data_files=[_RESPONSE_EADRAX_INITIATED])
+
+            backend_mock.add_response(
+                r'https://.+/eadrax-vrccs/v2/presentation/remote-commands/eventStatus\?eventId=.+',
+                data_files=[
+                    _RESPONSE_EADRAX_PENDING,
+                    _RESPONSE_EADRAX_DELIVERED,
+                    _RESPONSE_EADRAX_EXECUTED])
+
+            backend_mock.add_response(r'https://.+/webapi/v1/user/vehicles/{vin}/executeService'.format(vin=G31_VIN),
+                                      data_files=[_RESPONSE_LEGACY_INITIATED])
 
             backend_mock.add_response(
                 r'https://.+/webapi/v1/user/vehicles/{vin}/serviceExecutionStatus\?serviceType={service_type}'.format(
@@ -74,17 +95,20 @@ class TestRemoteServices(unittest.TestCase):
                 r'https://.+/webapi/v1/user/vehicles/{vin}/status'.format(
                     vin=G31_VIN),
                 data_files=[
-                    _RESPONSE_UNKNOWN,
-                    _RESPONSE_PENDING,
-                    _RESPONSE_PENDING,
-                    _RESPONSE_DELIVERED,
-                    _RESPONSE_DELIVERED,
-                    _RESPONSE_EXECUTED])
+                    _RESPONSE_LEGACY_UNKNOWN,
+                    _RESPONSE_LEGACY_PENDING,
+                    _RESPONSE_LEGACY_DELIVERED,
+                    _RESPONSE_LEGACY_EXECUTED])
+
+            # backend_mock.add_response(
+            #     r'https://.+/webapi/v1/user/vehicles/{vin}/status'.format(
+            #         vin=G31_VIN),
+            #     data_files=[_RESPONSE_LEGACY_EXECUTED])
 
             backend_mock.add_response(
-                r'https://.+/webapi/v1/user/vehicles/{vin}/status'.format(
-                    vin=G31_VIN),
-                data_files=[_RESPONSE_EXECUTED])
+                r'https://.+/eadrax-dcs/v1/send-to-car/send-to-car',
+                data_files=[_MSG_EXECUTED],
+                status_code=204)
 
             backend_mock.add_response(
                 r'https://.+/webapi/v1/user/vehicles/{vin}/sendpoi'.format(
@@ -99,7 +123,12 @@ class TestRemoteServices(unittest.TestCase):
                 vehicle = account.get_vehicle(G31_VIN)
 
                 if service == 'SEND_MESSAGE':
-                    response = getattr(vehicle.remote_services, call)(MESSAGE_DATA)
+                    if account.server_url_eadrax:
+                        with self.assertRaises(NotImplementedError):
+                            response = getattr(vehicle.remote_services, call)(MESSAGE_DATA)
+                        response = RemoteServiceStatus({"eventStatus": "EXECUTED"})
+                    else:
+                        response = getattr(vehicle.remote_services, call)(MESSAGE_DATA)
                 elif service == 'SEND_POI':
                     response = getattr(vehicle.remote_services, call)(POI_DATA)
                 else:
@@ -118,14 +147,35 @@ class TestRemoteServices(unittest.TestCase):
         with mock.patch('bimmer_connected.account.requests', new=backend_mock):
             account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
             vehicle = account.get_vehicle(G31_VIN)
-            with self.assertRaises(IOError):
-                vehicle.remote_services._get_remote_service_status(remote_services._Services.REMOTE_LIGHT_FLASH)
 
-            backend_mock.add_response(
-                r'https://.+/webapi/v1/user/vehicles/{vin}/serviceExecutionStatus\?.+'.format(vin=G31_VIN),
-                data_files=['G31_NBTevo/flash_executed.json'])
-            status = vehicle.remote_services._get_remote_service_status(remote_services._Services.REMOTE_LIGHT_FLASH)
-            self.assertEqual(ExecutionState.EXECUTED, status.state)
+            if account.server_url_eadrax:
+                backend_mock.add_response(
+                    r'https://.+/eadrax-vrccs/v2/presentation/remote-commands/eventStatus\?eventId=None',
+                    status_code=500,
+                    data='[]'
+                    )
+                with self.assertRaises(HTTPError):
+                    vehicle.remote_services._get_remote_service_status(remote_services._Services.REMOTE_LIGHT_FLASH)
+
+                backend_mock.add_response(
+                    r'https://.+/eadrax-vrccs/v2/presentation/remote-commands/eventStatus\?eventId=.+',
+                    data_files=[_RESPONSE_EADRAX_EXECUTED])
+
+                status = vehicle.remote_services._get_remote_service_status(event_id="000000")
+                self.assertEqual(ExecutionState.EXECUTED, status.state)
+
+            else:
+                with self.assertRaises(IOError):
+                    vehicle.remote_services._get_remote_service_status(remote_services._Services.REMOTE_LIGHT_FLASH)
+
+                backend_mock.add_response(
+                    r'https://.+/webapi/v1/user/vehicles/{vin}/serviceExecutionStatus\?.+'.format(vin=G31_VIN),
+                    data_files=[_RESPONSE_LEGACY_EXECUTED])
+
+                status = vehicle.remote_services._get_remote_service_status(
+                    remote_services._Services.REMOTE_LIGHT_FLASH
+                )
+                self.assertEqual(ExecutionState.EXECUTED, status.state)
 
     def test_parsing_of_poi_min_attributes(self):
         """Check that a PointOfInterest can be constructed using only latitude & longitude."""