Repository: G-Two/subarulink (language: python)

Issue:
Second Phase Connect to mysubaru.com Failing
Hello and thanks for subarulink.
I'm getting an error when using the CLI, whether it's doing the initial interactive setup or when sending a command.
My accounts were set up a week ago on 3/12. 
The process did demand that I set up 2-factor authentication.
I do get data back from https://mobileapi.prod.subarucs.com , but there is an error on trying to connect to mysubaru.com:

> ...
>           'vehicleName': '2022 Crosstrek Limited',
>           'vin': 'JFXXXXXXXXXXXXXXX',
>           'zip': None},
>  'dataName': 'vehicle',
>  'errorCode': None,
>  'success': True}
> 2022-03-19 16:26:43,386 - subarulink.connection - DEBUG - Authorizing device via web API
> 2022-03-19 16:26:43,387 - subarulink.connection - DEBUG - POST: https://www.mysubaru.com/login, params=None, json_data=None
> 2022-03-19 16:26:43,646 - subarulink.connection - ERROR - RequestInfo(url=URL('https://www.mysubaru.com/login'), method='POST', headers=<CIMultiDictProxy('Host': 'www.mysubaru.com', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Python/3.9 aiohttp/3.8.1', 'Content-Length': '77', 'Content-Type': 'application/x-www-form-urlencoded')>, real_url=URL('https://www.mysubaru.com/login'))
> 2022-03-19 16:26:43,783 - subarulink.connection - ERROR - ('\r\n'
>  '\r\n'
>  '\r\n'
> ...

...plus the remainder of the raw HTML of the response, then:

>  '</html>\r\n')
> 2022-03-19 16:26:43,785 - subarulink - ERROR - Unable to connect: HTTP 500: <Cli
> entResponse(https://www.mysubaru.com/login) [500 ]>

Could the 2-factor be causing the problem? I don't find any way to disable it.
Multiple vehicles in account but VIN not specified in config
Hey!

I'm getting:

> 2022-03-24 08:29:45,439 - subarulink - ERROR - Multiple vehicles in account but VIN not specified in config or command line


however:

```
-> % cat ~/.subarulink.cfg
{
    "country": "USA",
    "username": "my.email@gmail.com",
    "password": "53cr3tp455w0rd",
    "pin": "1234",
    "device_name": "subarulink",
    "save_creds": "Y",
    "device_id": 1628400000,
    "default_vin": "JF2XXXXX1XX123456",
    "remote_start_preset": "FULL HEAT"
}
```

Obviously I've masked out sensitive values, but that is otherwise my config.  I'm no sure how to specify key/value on the command line as it's not mentioned in `--help`.  My checkout is current:

```
-> % git pull
Already up to date.

-> % git log | head -n1
commit e60d054cb9b68cd6898fa4b7a53cae108188992f

-> % python --version
Python 3.9.5
```

I also really only have one car in my account
![Screen Shot 2022-03-24 at 8 35 52 AM](https://user-images.githubusercontent.com/82891848/159917484-448025ab-99a6-4ca5-8b50-49f839236d2f.png)


THANK YOU!
M.



Discussion / hints:

Fix patch:
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 2e9d9d8..5e4cc31 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -6,7 +6,7 @@ The `subarulink` package provides a `Controller` class that manages a connection
     - `websession` - `aiohttp.ClientSession` instance
     - `username` - Your MySubaru account username, normally an email address
     - `password` - Your MySubaru account password
-    - `device_id` - An identifier string for the device accessing the Subaru API.  The web browser interface uses the integer value (as a string) of the timestamp when the user first logged in.  The Android app uses some sort of hexadecimal string.  It doesn't seem to matter what the content of this string is.  The important thing is to consistently use the same one for a given MySubaru account when using this package.  Once a device is "authorized" to perform remote commands, it will appear as one of your authorized devices in your MySubaru profile.  If you do not use the same `device_id` over time, additional entries will appear in your MySubaru profile each time you login.
+    - `device_id` - An identifier string for the device accessing the Subaru API.  The web browser interface uses the integer value (as a string) of the timestamp when the user first logged in.  The Android app uses some sort of hexadecimal string.  It doesn't seem to matter what the content of this string is.  The important thing is to consistently use the same one for a given MySubaru account when using this package.  Once a device is authorized via 2-Factor Authentication, it will appear as one of your authorized devices in your MySubaru profile.  If you do not use the same `device_id` over time, you will need to revalidate via 2FA each time you login, and additional entries will appear in your MySubaru profile each time you login.
     - `pin` - The 4-digit PIN number your vehicle.
     - `device_name` - A human readable string that maps to a particular `device_id`.  This is the string that is shown in your MySubaru profile list of authorized devices.
     - `country` - Country used for MySubaru registration.  Currently `"USA"` and `"CAN"` are supported.
@@ -14,7 +14,16 @@ The `subarulink` package provides a `Controller` class that manages a connection
     - `fetch_interval` -  Number of seconds between fetches of Subaru's cached vehicle information. Used to prevent excessive polling of Subaru API.  
 
 The connect method will authenticate to Subaru servers and perform the necessary initialization and API queries to be ready for subsequent API calls.
-- `Controller.connect(test_login=False)` - Returns `True` upon success. If `test_login=True`, the controller will authenticate only and not be ready for subsequent API calls.
+- `Controller.connect()` - Returns `True` upon success.
+
+The Subaru API uses 2FA (via SMS or email) to register devices, including applications using this package. If a device is not registered, it will not be allowed to perform most API calls.
+- `Controller.device_registered` - If this property is `False`, 2FA needs to be performed. If `True` then 2FA has been completed for this session and/or 2FA was completed with the `make_permanent` option set.
+
+To perform 2FA, a validation code must be requested with:
+- `Controller.request_auth_code(contact_method)` - Where `contact_method` is a string from the `Controller.auth_contact_methods` list. This function returns `True` if successful. After calling this function check your mobile phone or email for the validation code.
+
+To submit received validation code, use:
+- `Controller.submit_auth_code(code, make_permanent=True)` - Where `code` is a 6-digit numeric validation code, and `make_permanent` is a boolean to permanently register the device (identified by `Controller.device_id`) with Subaru so that 2FA is no longer required for this device.
 
 A list of vehicles on your MySubaru account may be obtained with:
 - `Controller.get_vehicles()` - Returns a list of VIN strings.  
diff --git a/README.md b/README.md
index be886fb..1c1c63b 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,15 @@ This package supports Subaru STARLINK equipped vehicles with active service plan
 
 There is a Home Assistant [custom component](https://github.com/G-Two/homeassistant-subaru) that uses this package and allows users to add Subaru STARLINK integration to their Home Assistant instance.
 
-In addition, as of 2021.3, Home Assistant Core includes the [Subaru integration](https://www.home-assistant.io/integrations/subaru/) that uses this package. Due to the required incremental additions required by Home Assistant Core, only the sensor platform is supported at this time. Additional PRs are pending to add full functionality. Users that desire the most recent features should continue using the custom component.
+In addition, as of 2021.3, Home Assistant Core includes the [Subaru integration](https://www.home-assistant.io/integrations/subaru/) that uses this package. Due to the required incremental additions required by Home Assistant Core, only the sensor and lock platforms are supported at this time. Additional PRs are pending to add full functionality. Users that desire the most recent features should continue using the custom component.
 
 ## Standalone Installation
 To use this module's included standalone console application or include the package in an application, install from PyPI:
 
     $ pip install subarulink
 
+For API documentation, see [DEVELOPERS.md](DEVELOPERS.md)
+
 ## Usage
 The PyPI installation includes a basic console application.  The application can either be run interactively or used to issue a single command.  The single command function requires a working config file to function properly (config file is automatically created during the first interactive run).  Note that not all exposed functions are supported by all vehicles. Consult your subscription details to determine which commands apply to your vehicle.
 
@@ -41,7 +43,7 @@ optional arguments:
   -h, --help            show this help message and exit
   -i, --interactive     interactive mode
   -c CONFIG_FILE, --config CONFIG_FILE
-                        specify config file (default is ~/.subarulink.cfg
+                        specify config file (default is ~/.config/subarulink/subarulink.cfg
   -v {0,1,2}, --verbosity {0,1,2}
                         verbosity level: 0=error[default] 1=info 2=debug
 
@@ -65,7 +67,7 @@ STARLINK accounts with multiple vehicles will need to specify the VIN for single
 Accounts with only one vehicle do not need to specify a VIN
 
 ## Configuration
-A JSON file is used for configuration. A user provided file can be passed to the CLI via the `--config`. If no config file is provided, two default locations are searched for. First is `~/.subarulink.cfg` and if that is not found, `$XDG_CONFIG_DIR/subarulink/subarulink.cfg` (`XDG_CONFIG_HOME` will be used.
+A JSON file is used for configuration. A user provided file can be passed to the CLI via the `--config`. If no config file is provided, two default locations are searched for. First is `~/.subarulink.cfg` and if that is not found, `$XDG_CONFIG_HOME/subarulink/subarulink.cfg` will be used.
 
 ## Known Issues
 ### Battery Discharge
diff --git a/setup.py b/setup.py
index 3d57620..68f9243 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@
 URL = "https://github.com/G-Two/subarulink"
 EMAIL = ""
 AUTHOR = "G-Two"
-REQUIRES_PYTHON = ">=3.7"
+REQUIRES_PYTHON = ">=3.9"
 LICENSE = "Apache-2.0"
 VERSION = None
 
@@ -121,10 +121,8 @@ def run(self):
         "Development Status :: 3 - Alpha",
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Internet',
     ],
     # $ setup.py publish support.
diff --git a/subarulink/__version__.py b/subarulink/__version__.py
index 465d45a..22e6fbd 100644
--- a/subarulink/__version__.py
+++ b/subarulink/__version__.py
@@ -5,4 +5,4 @@
 For more details about this api, please refer to the documentation at
 https://github.com/G-Two/subarulink
 """
-__version__ = "0.4.3"
+__version__ = "0.5.0"
diff --git a/subarulink/app/cli.py b/subarulink/app/cli.py
index 9cd27fe..88b36c3 100644
--- a/subarulink/app/cli.py
+++ b/subarulink/app/cli.py
@@ -129,12 +129,23 @@ async def _quit(self, code):
         sys.exit(code)
 
     async def _vehicle_select(self, interactive=True, vin=None, reselect=False):
-        if (interactive and self.config.get("default_vin") is None) or reselect:
+        if len(self._cars) == 0:
+            LOGGER.error(
+                "No vehicles are associated with this account. If this is incorrect, there may be a temporary issue with the Subaru API. Please try again."
+            )
+            await self._quit(1)
+
+        elif len(self._cars) == 1:
+            self._current_vin = self._cars[0]
+
+        elif (interactive and self.config.get("default_vin") is None) or reselect:
             while True:
                 selected = -1
                 print("\nAvailable Vehicles:")
                 for index, _vin in enumerate(self._cars):
-                    print("[%d] %s (%s)" % (index + 1, self._ctrl.vin_to_name(_vin), _vin))
+                    print(
+                        f"[{index + 1}] {self._ctrl.vin_to_name(_vin)} ({_vin}) - {self._ctrl.get_model_year(_vin)} {self._ctrl.get_model_name(_vin)}"
+                    )
                 if len(self._cars) == 1:
                     selected = 0
                 if selected == -1:
@@ -152,16 +163,21 @@ async def _vehicle_select(self, interactive=True, vin=None, reselect=False):
                 self._current_vin = vin
             else:
                 LOGGER.error("VIN %s does not exist in user account.", vin)
-                await self._quit(3)
-
-        elif len(self._cars) == 1:
-            self._current_vin = self._cars[0]
+                await self._quit(1)
 
         elif self.config.get("default_vin") in self._cars:
             self._current_vin = self.config.get("default_vin")
 
+        elif self.config.get("default_vin") not in self._cars:
+            LOGGER.error("VIN %s does not exist in user account.", self.config.get("default_vin"))
+            await self._quit(1)
+
+        elif len(self._cars) > 1:
+            LOGGER.error("Multiple vehicles in account but VIN not specified in config or command line (with --vin)")
+            await self._quit(1)
+
         else:
-            LOGGER.error("Multiple vehicles in account but VIN not specified in config or command line")
+            LOGGER.error("Something unexpected happened. Use -v2 for more debug information.")
             await self._quit(1)
 
         await self._fetch()
@@ -372,6 +388,7 @@ async def _connect(self, interactive=True, vin=None):
         try:
             if await self._ctrl.connect():
                 LOGGER.info("Successfully connected")
+                await self._register_device()
                 self._cars = self._ctrl.get_vehicles()
                 await self._vehicle_select(interactive, vin)
                 if interactive:
@@ -384,6 +401,23 @@ async def _connect(self, interactive=True, vin=None):
             return False
         return True
 
+    async def _register_device(self):
+        attempts_left = 3
+        if not self._ctrl.device_registered:
+            method = _select_from_list(
+                "This device is not recognized. Request new 2FA code to be sent to",
+                list(self._ctrl.contact_methods.items()),
+            )[0]
+            await self._ctrl.request_auth_code(method)
+            while attempts_left:
+                code = input(f"Enter 2FA code received at {self._ctrl.contact_methods[method]}: ")
+                if await self._ctrl.submit_auth_code(code):
+                    return True
+                attempts_left -= 1
+                LOGGER.error("Verification failed, %d/3 attempts remaining.", attempts_left)
+            raise SubaruException("Maximum 2FA attempts exceeded")
+        return True
+
     async def _cli_loop(self):
         print("\nEnter a command. For a list of commands, enter '?'.")
         running = True
diff --git a/subarulink/connection.py b/subarulink/connection.py
index e99fd07..c3c7fb7 100644
--- a/subarulink/connection.py
+++ b/subarulink/connection.py
@@ -13,6 +13,9 @@
 from yarl import URL
 
 from subarulink.const import (
+    API_2FA_AUTH_VERIFY,
+    API_2FA_CONTACT,
+    API_2FA_SEND_VERIFICATION,
     API_LOGIN,
     API_SELECT_VEHICLE,
     API_VALIDATE_SESSION,
@@ -22,10 +25,6 @@
     MOBILE_API_SERVER,
     MOBILE_API_VERSION,
     MOBILE_APP,
-    WEB_API_AUTHORIZE_DEVICE,
-    WEB_API_LOGIN,
-    WEB_API_NAME_DEVICE,
-    WEB_API_SERVER,
 )
 from subarulink.exceptions import (
     IncompleteCredentials,
@@ -83,14 +82,12 @@ def __init__(
         self._current_vin = None
         self._list_of_vins = []
         self._session_login_time = None
+        self._auth_contact_options = None
 
-    async def connect(self, test_login=False):
+    async def connect(self):
         """
         Connect to and establish session with Subaru Starlink mobile app API.
 
-        Args:
-            test_login (bool): If `True` then username/password is verified but device registration is not performed.
-
         Returns:
             List: A list of strs containing the VIN of each vehicle registered in the Subaru account.
 
@@ -102,17 +99,65 @@ async def connect(self, test_login=False):
         """
         await self._authenticate()
         await self._get_vehicle_data()
-        if self._registered or test_login:
-            return self._vehicles
-        if await self._register_device():
-            self.reset_session()
-            while not self._registered:
-                # Device registration does not always immediately take effect
-                await asyncio.sleep(3)
-                await self._authenticate()
-                # Current server side vin context is ambiguous (even for single vehicle account??)
-                self._current_vin = None
-            return self._vehicles
+        if not self.device_registered:
+            await self._get_contact_methods()
+        return self._vehicles
+
+    @property
+    def device_registered(self):
+        """Device is registered."""
+        return self._registered
+
+    @property
+    def auth_contact_methods(self):
+        """Contact methods for 2FA."""
+        return self._auth_contact_options
+
+    async def request_auth_code(self, contact_method):
+        """Request 2FA code be sent via specified contact method."""
+        if contact_method not in self.auth_contact_methods:
+            _LOGGER.error("Invalid 2FA contact method requested")
+            return False
+        _LOGGER.debug("Requesting 2FA code")
+        post_data = {
+            "contactMethod": contact_method,
+            "languagePreference": "EN",
+        }
+        js_resp = await self.__open(
+            API_2FA_SEND_VERIFICATION,
+            POST,
+            params=post_data,
+        )
+        if js_resp:
+            _LOGGER.debug(pprint.pformat(js_resp))
+            return True
+
+    async def submit_auth_code(self, code, make_permanent=True):
+        """Submit received 2FA code for validation."""
+        if not code.isdecimal() or len(code) != 6:
+            _LOGGER.error("2FA code must be 6 digits")
+            return False
+        _LOGGER.info("Validating 2FA response")
+        post_data = {
+            "deviceId": self._device_id,
+            "deviceName": self._device_name,
+            "verificationCode": code,
+        }
+        if make_permanent:
+            post_data["rememberDevice"] = "on"
+
+        js_resp = await self.__open(API_2FA_AUTH_VERIFY, POST, params=post_data)
+        if js_resp:
+            _LOGGER.debug(pprint.pformat(js_resp))
+            if js_resp["success"]:
+                _LOGGER.info("Device successfully authorized")
+                while not self._registered:
+                    # Device registration does not always immediately take effect
+                    await asyncio.sleep(3)
+                    await self._authenticate()
+                    # Current server side vin context is ambiguous (even for single vehicle account??)
+                    self._current_vin = None
+                return True
 
     async def validate_session(self, vin):
         """
@@ -258,45 +303,11 @@ async def _get_vehicle_data(self):
             self._vehicles.append(js_resp["data"])
             self._current_vin = vin
 
-    async def _register_device(self):
-        _LOGGER.debug("Authorizing device via web API")
-        post_data = {
-            "username": self._username,
-            "password": self._password,
-            "deviceId": self._device_id,
-        }
-        resp = await self.__open(
-            WEB_API_LOGIN,
-            POST,
-            data=post_data,
-            baseurl=f"https://{WEB_API_SERVER[self._country]}",
-            decode_json=False,
-        )
-        js_resp = None
-        if resp:
-            js_resp = await self.__open(
-                WEB_API_AUTHORIZE_DEVICE,
-                GET,
-                params={"deviceId": self._device_id},
-                baseurl=f"https://{WEB_API_SERVER[self._country]}",
-                decode_json=False,
-            )
-        if js_resp:
-            _LOGGER.info("Device successfully authorized")
-            return await self._set_device_name()
-
-    async def _set_device_name(self):
-        _LOGGER.debug("Setting Device Name to %s", self._device_name)
-        js_resp = await self.__open(
-            WEB_API_NAME_DEVICE,
-            GET,
-            params={"deviceId": self._device_id, "deviceName": self._device_name},
-            baseurl=f"https://{WEB_API_SERVER[self._country]}",
-            decode_json=False,
-        )
+    async def _get_contact_methods(self):
+        js_resp = await self.__open(API_2FA_CONTACT, POST)
         if js_resp:
-            _LOGGER.debug("Set Device Name Successful")
-            return True
+            _LOGGER.debug(pprint.pformat(js_resp))
+            self._auth_contact_options = js_resp.get("data")
 
     async def __open(
         self,
@@ -307,7 +318,6 @@ async def __open(
         json_data=None,
         params=None,
         baseurl="",
-        decode_json=True,
     ):
         """Open url."""
         if not baseurl:
@@ -321,15 +331,11 @@ async def __open(
                     url, headers=headers, params=params, data=data, json=json_data
                 )
                 if resp.status > 299:
-                    _LOGGER.error(pprint.pformat(resp.request_info))
-                    _LOGGER.error(pprint.pformat(await resp.text()))
-                    raise SubaruException("HTTP %d: %s" % (resp.status, resp))
-                if decode_json:
-                    js_resp = await resp.json()
-                    if "success" not in js_resp and "serviceType" not in js_resp:
-                        raise SubaruException("Unexpected response: %s" % resp)
-                    return js_resp
-                return resp
+                    raise SubaruException("HTTP %d: %s %s" % (resp.status, resp.text(), resp.request_info))
+                js_resp = await resp.json()
+                if "success" not in js_resp and "serviceType" not in js_resp:
+                    raise SubaruException("Unexpected response: %s" % resp)
+                return js_resp
             except aiohttp.ClientResponseError as err:
                 raise SubaruException(err.status) from err
             except aiohttp.ClientConnectionError as err:
diff --git a/subarulink/const.py b/subarulink/const.py
index fa9623f..6f2e79b 100644
--- a/subarulink/const.py
+++ b/subarulink/const.py
@@ -19,13 +19,11 @@
     COUNTRY_CAN: "ca.subaru.telematics.remote",
 }
 
-WEB_API_SERVER = {COUNTRY_USA: "www.mysubaru.com", COUNTRY_CAN: "www.mysubaru.ca"}
-WEB_API_LOGIN = "/login"
-WEB_API_AUTHORIZE_DEVICE = "/profile/updateDeviceEntry.json"
-WEB_API_NAME_DEVICE = "/profile/addDeviceName.json"
-
 # Same API for g1 and g2
 API_LOGIN = "/login.json"
+API_2FA_CONTACT = "/twoStepAuthContacts.json"
+API_2FA_SEND_VERIFICATION = "/twoStepAuthSendVerification.json"
+API_2FA_AUTH_VERIFY = "/twoStepAuthVerify.json"
 API_REFRESH_VEHICLES = "/refreshVehicles.json"
 API_SELECT_VEHICLE = "/selectVehicle.json"
 API_VALIDATE_SESSION = "/validateSession.json"
@@ -298,6 +296,7 @@
 ERROR_TOO_MANY_ATTEMPTS = "tooManyAttempts"
 ERROR_VEHICLE_NOT_IN_ACCOUNT = "vehicleNotInAccount"
 ERROR_INVALID_TOKEN = "InvalidToken"
+ERROR_VEHICLE_SETUP = "VEHICLESETUPERROR"
 
 # G1 Error Codes
 ERROR_G1_NO_SUBSCRIPTION = "SXM40004"
@@ -310,6 +309,8 @@
 VEHICLE_ATTRIBUTES = "attributes"
 VEHICLE_STATUS = "status"
 VEHICLE_ID = "id"
+VEHICLE_MODEL_NAME = "modelName"
+VEHICLE_MODEL_YEAR = "modelYear"
 VEHICLE_NAME = "nickname"
 VEHICLE_API_GEN = "api_gen"
 VEHICLE_LOCK = "lock"
diff --git a/subarulink/controller.py b/subarulink/controller.py
index 43e73e0..45a4887 100644
--- a/subarulink/controller.py
+++ b/subarulink/controller.py
@@ -64,13 +64,10 @@ def __init__(
         self._pin_lockout = False
         self.version = subarulink.__version__
 
-    async def connect(self, test_login=False):
+    async def connect(self):
         """
         Connect to Subaru Remote Services API.
 
-        Args:
-            test_login (bool, optional): If `True` then username/password is verified only.
-
         Returns:
             bool: `True` if success, `False` if failure
 
@@ -81,14 +78,34 @@ async def connect(self, test_login=False):
         """
         _LOGGER.debug("subarulink %s", self.version)
         _LOGGER.debug("Connecting controller to Subaru Remote Services")
-        vehicle_list = await self._connection.connect(test_login=test_login)
+        vehicle_list = await self._connection.connect()
 
-        if not test_login:
+        if len(vehicle_list) > 0:
             for vehicle in vehicle_list:
                 self._parse_vehicle(vehicle)
             _LOGGER.debug("Subaru Remote Services Ready")
+            return True
 
-        return True
+        _LOGGER.debug("No vehicles found, nothing to do")
+        return False
+
+    @property
+    def device_registered(self):
+        """Device is registered."""
+        return self._connection.device_registered
+
+    @property
+    def contact_methods(self):
+        """Email address for 2FA."""
+        return self._connection.auth_contact_methods
+
+    async def request_auth_code(self, contact_method):
+        """Request 2FA code be sent via email."""
+        return await self._connection.request_auth_code(contact_method)
+
+    async def submit_auth_code(self, code):
+        """Submit received 2FA code for validation."""
+        return await self._connection.submit_auth_code(code)
 
     def is_pin_required(self):
         """
@@ -138,6 +155,34 @@ def get_vehicles(self):
         """
         return list(self._vehicles.keys())
 
+    def get_model_year(self, vin):
+        """
+        Get model year for the specified VIN.
+
+        Args:
+            vin (str): The VIN to check.
+
+        Returns:
+            str: model year.
+            None: If `vin` is invalid.
+        """
+        if isinstance(vehicle := self._vehicles.get(vin.upper()), dict):
+            return vehicle.get(sc.VEHICLE_MODEL_YEAR)
+
+    def get_model_name(self, vin):
+        """
+        Get model name for the specified VIN.
+
+        Args:
+            vin (str): The VIN to check.
+
+        Returns:
+            str: model name.
+            None: If `vin` is invalid.
+        """
+        if isinstance(vehicle := self._vehicles.get(vin.upper()), dict):
+            return vehicle.get(sc.VEHICLE_MODEL_NAME)
+
     def get_ev_status(self, vin):
         """
         Get whether the specified VIN is an Electric Vehicle.
@@ -801,6 +846,8 @@ def _parse_vehicle(self, vehicle):
         vin = vehicle["vin"].upper()
         _LOGGER.debug("Parsing vehicle: %s", vin)
         self._vehicles[vin] = {
+            sc.VEHICLE_MODEL_YEAR: vehicle[sc.VEHICLE_MODEL_YEAR],
+            sc.VEHICLE_MODEL_NAME: vehicle[sc.VEHICLE_MODEL_NAME],
             sc.VEHICLE_NAME: vehicle[sc.VEHICLE_NAME],
             sc.VEHICLE_LOCK: asyncio.Lock(),
             sc.VEHICLE_LAST_FETCH: 0,
@@ -926,13 +973,21 @@ async def _fetch_status(self, vin):
 
         # Additional Data (Security Plus and Generation2 Required)
         if self.get_remote_status(vin) and self.get_api_gen(vin) == sc.FEATURE_G2_TELEMATICS:
-            js_resp = await self._remote_query(vin, sc.API_CONDITION)
-            if js_resp.get("success") and js_resp.get("data"):
-                status = await self._cleanup_condition(js_resp, vin)
-                self._vehicles[vin][sc.VEHICLE_STATUS].update(status)
-
-            # Obtain lat/long from a more reliable source for Security Plus g2
-            await self._locate(vin)
+            try:
+                js_resp = await self._remote_query(vin, sc.API_CONDITION)
+                if js_resp.get("success") and js_resp.get("data"):
+                    status = await self._cleanup_condition(js_resp, vin)
+                    self._vehicles[vin][sc.VEHICLE_STATUS].update(status)
+
+                # Obtain lat/long from a more reliable source for Security Plus g2
+                await self._locate(vin)
+
+            except SubaruException as err:
+                if "HTTP 500" in err.message:
+                    # This is a condition that intermittently occurs and appears to be caused by some sort of timeout on the Subaru backend
+                    _LOGGER.warning("HTTP 500 received when fetching vehicle information from Subaru")
+                    return False
+                raise err
 
         # Fetch climate presets for supported vehicles
         if self.get_res_status(vin) or self.get_ev_status(vin):
@@ -974,6 +1029,7 @@ async def _cleanup_condition(self, js_resp, vin):
                 )
             else:
                 data[sc.EV_TIME_TO_FULLY_CHARGED_UTC] = None
+            data[sc.EV_TIME_TO_FULLY_CHARGED] = data[sc.EV_TIME_TO_FULLY_CHARGED_UTC]
 
         # check for other g2 known erroneous values
         if data.get(sc.EXTERNAL_TEMP) == sc.BAD_EXTERNAL_TEMP:


Test patch:
diff --git a/tests/api_responses.py b/tests/api_responses.py
index 666e0f6..01b7d2d 100644
--- a/tests/api_responses.py
+++ b/tests/api_responses.py
@@ -1248,3 +1248,10 @@
     "errorCode": sc.ERROR_VEHICLE_NOT_IN_ACCOUNT,
     "success": False,
 }
+
+ERROR_VEHICLE_SETUP = {
+    "data": None,
+    "dataName": None,
+    "errorCode": sc.ERROR_VEHICLE_SETUP,
+    "success": False,
+}
diff --git a/tests/conftest.py b/tests/conftest.py
index 4923c60..853c188 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -75,7 +75,6 @@ async def test_server(ssl_certificate):
 async def controller(test_server, http_redirect):
     """Return a test controller that talks to a local test server."""
     http_redirect.add_server(sc.MOBILE_API_SERVER[sc.COUNTRY_USA], 443, test_server.port)
-    http_redirect.add_server(sc.WEB_API_SERVER[sc.COUNTRY_USA], 443, test_server.port)
     controller = subarulink.Controller(
         http_redirect.session,
         TEST_USERNAME,
diff --git a/tests/test_connection.py b/tests/test_connection.py
index 71f76b3..a195e5b 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -11,6 +11,7 @@
 from tests.api_responses import (
     CONDITION_G2,
     ERROR_403,
+    ERROR_VEHICLE_SETUP,
     ERROR_VIN_NOT_FOUND,
     FETCH_SUBARU_CLIMATE_PRESETS,
     FETCH_USER_CLIMATE_PRESETS_EV,
@@ -101,7 +102,7 @@ async def test_handle_404(test_server, controller):
 
 
 @pytest.mark.asyncio
-async def test_connect_device_registration(test_server, controller):
+async def test_connect_device_registration_success(test_server, controller):
     task = asyncio.create_task(controller.connect())
 
     await server_js_response(test_server, LOGIN_SINGLE_NOT_REGISTERED, path=sc.API_LOGIN)
@@ -111,15 +112,44 @@ async def test_connect_device_registration(test_server, controller):
         path=sc.API_SELECT_VEHICLE,
         query={"vin": TEST_VIN_1_G1, "_": str(int(time.time()))},
     )
-    await server_js_response(test_server, True, path=sc.WEB_API_LOGIN)
-    await server_js_response(test_server, True, path=sc.WEB_API_AUTHORIZE_DEVICE)
-    await server_js_response(test_server, True, path=sc.WEB_API_NAME_DEVICE)
+    await server_js_response(
+        test_server, {"success": True, "data": {"userName": "test@test.com"}}, path=sc.API_2FA_CONTACT
+    )
+    assert await task
+
+    task = asyncio.create_task(controller.request_auth_code("userName"))
+    await server_js_response(test_server, {"success": True}, path=sc.API_2FA_SEND_VERIFICATION)
+    assert await task
+
+    task = asyncio.create_task(controller.submit_auth_code("123456"))
+    await server_js_response(test_server, {"success": True}, path=sc.API_2FA_AUTH_VERIFY)
     await server_js_response(test_server, LOGIN_SINGLE_NOT_REGISTERED, path=sc.API_LOGIN)
     await server_js_response(test_server, LOGIN_SINGLE_NOT_REGISTERED, path=sc.API_LOGIN)
     await server_js_response(test_server, LOGIN_SINGLE_REGISTERED, path=sc.API_LOGIN)
+    assert await task
 
-    response = await task
-    assert response
+
+@pytest.mark.asyncio
+async def test_connect_device_registration_bad_input(test_server, controller):
+    task = asyncio.create_task(controller.connect())
+
+    await server_js_response(test_server, LOGIN_SINGLE_NOT_REGISTERED, path=sc.API_LOGIN)
+    await server_js_response(
+        test_server,
+        SELECT_VEHICLE_1,
+        path=sc.API_SELECT_VEHICLE,
+        query={"vin": TEST_VIN_1_G1, "_": str(int(time.time()))},
+    )
+    await server_js_response(
+        test_server, {"success": True, "data": {"userName": "test@test.com"}}, path=sc.API_2FA_CONTACT
+    )
+    assert await task
+
+    task = asyncio.create_task(controller.request_auth_code("invalid_auth_type"))
+    assert not await task
+
+    task = asyncio.create_task(controller.submit_auth_code("invalid_code_format"))
+    assert not await task
 
 
 @pytest.mark.asyncio
@@ -149,7 +179,7 @@ async def test_connect_multi_car(multi_vehicle_controller):
 @pytest.mark.asyncio
 async def test_login_fail(test_server, controller):
     for fail_msg in LOGIN_ERRORS:
-        task = asyncio.create_task(controller.connect(test_login=True))
+        task = asyncio.create_task(controller.connect())
         await server_js_response(test_server, fail_msg, path=sc.API_LOGIN)
         with pytest.raises(subarulink.SubaruException):
             await task
@@ -236,6 +266,42 @@ async def test_switch_vehicle_fail(test_server, multi_vehicle_controller):
         await task
 
 
+@pytest.mark.asyncio
+async def test_switch_vehicle_setup_fail(test_server, multi_vehicle_controller):
+    task = asyncio.create_task(multi_vehicle_controller.horn(TEST_VIN_2_EV))
+
+    await server_js_response(test_server, VALIDATE_SESSION_SUCCESS, path=sc.API_VALIDATE_SESSION)
+    await server_js_response(
+        test_server,
+        ERROR_VEHICLE_SETUP,
+        path=sc.API_SELECT_VEHICLE,
+        query={"vin": TEST_VIN_2_EV, "_": str(int(time.time()))},
+    )
+    await server_js_response(test_server, LOGIN_SINGLE_REGISTERED, path=sc.API_LOGIN)
+    await server_js_response(
+        test_server,
+        SELECT_VEHICLE_2,
+        path=sc.API_SELECT_VEHICLE,
+        query={"vin": TEST_VIN_2_EV, "_": str(int(time.time()))},
+    )
+    await server_js_response(
+        test_server,
+        REMOTE_SERVICE_EXECUTE,
+        path=sc.API_HORN_LIGHTS,
+    )
+    await server_js_response(
+        test_server,
+        REMOTE_SERVICE_STATUS_STARTED,
+        path=sc.API_REMOTE_SVC_STATUS,
+    )
+    await server_js_response(
+        test_server,
+        REMOTE_SERVICE_STATUS_FINISHED_SUCCESS,
+        path=sc.API_REMOTE_SVC_STATUS,
+    )
+    assert await task
+
+
 @pytest.mark.asyncio
 async def test_expired_session(test_server, multi_vehicle_controller):
     task = asyncio.create_task(multi_vehicle_controller.horn(TEST_VIN_3_G2))