diff --git a/bimmer_connected/account.py b/bimmer_connected/account.py index 67e7774c..5c5d2caf 100644 --- a/bimmer_connected/account.py +++ b/bimmer_connected/account.py @@ -12,9 +12,9 @@ from bimmer_connected.api.client import MyBMWClient, MyBMWClientConfiguration from bimmer_connected.api.regions import Regions from bimmer_connected.const import VEHICLES_URL, CarBrands +from bimmer_connected.models import GPSPosition from bimmer_connected.utils import deprecated from bimmer_connected.vehicle import MyBMWVehicle -from bimmer_connected.vehicle.models import GPSPosition VALID_UNTIL_OFFSET = datetime.timedelta(seconds=10) @@ -34,29 +34,34 @@ class MyBMWAccount: # pylint: disable=too-many-instance-attributes region: Regions """Region of the account. See `api.Regions`.""" - mybmw_client_config: MyBMWClientConfiguration = None # type: ignore[assignment] + config: MyBMWClientConfiguration = None # type: ignore[assignment] """Optional. If provided, username/password/region are ignored.""" log_responses: InitVar[pathlib.Path] = None """Optional. If set, all responses from the server will be logged to this directory.""" - observer_position: Optional[GPSPosition] = None + observer_position: InitVar[GPSPosition] = None """Optional. Required for getting a position on older cars.""" + use_metric_units: InitVar[bool] = True + """Optional. Use metric units (km, l) by default. Use imperial units (mi, gal) if False.""" + vehicles: List[MyBMWVehicle] = field(default_factory=list, init=False) - def __post_init__(self, password, log_responses): - if self.mybmw_client_config is None: - self.mybmw_client_config = MyBMWClientConfiguration( + def __post_init__(self, password, log_responses, observer_position, use_metric_units): + if self.config is None: + self.config = MyBMWClientConfiguration( MyBMWAuthentication(self.username, password, self.region), log_response_path=log_responses, + observer_position=observer_position, + use_metric_units=use_metric_units, ) async def get_vehicles(self) -> None: """Retrieve vehicle data from BMW servers.""" _LOGGER.debug("Getting vehicle list") - async with MyBMWClient(self.mybmw_client_config) as client: + async with MyBMWClient(self.config) as client: vehicles_request_params = { "apptimezone": self.utcdiff, "appDateTime": int(datetime.datetime.now().timestamp() * 1000), @@ -66,7 +71,7 @@ async def get_vehicles(self) -> None: await client.get( VEHICLES_URL, params=vehicles_request_params, - headers=client.generate_default_header(self.region, brand), + headers=client.generate_default_header(brand), ) for brand in CarBrands ] @@ -94,11 +99,15 @@ def get_vehicle(self, vin: str) -> Optional[MyBMWVehicle]: def set_observer_position(self, latitude: float, longitude: float) -> None: """Set the position of the observer for all vehicles.""" - self.observer_position = GPSPosition(latitude=latitude, longitude=longitude) + self.config.observer_position = GPSPosition(latitude=latitude, longitude=longitude) def set_refresh_token(self, refresh_token: str) -> None: """Overwrite the current value of the MyBMW refresh token.""" - self.mybmw_client_config.authentication.refresh_token = refresh_token + self.config.authentication.refresh_token = refresh_token + + def set_use_metric_units(self, use_metric_units: bool) -> None: + """Change between using metric units (km, l) if True or imperial units (mi, gal) if False.""" + self.config.use_metric_units = use_metric_units @property def timezone(self): @@ -113,7 +122,7 @@ def utcdiff(self): @property def refresh_token(self) -> Optional[str]: """Returns the current refresh_token.""" - return self.mybmw_client_config.authentication.refresh_token + return self.config.authentication.refresh_token @deprecated("MyBMWAccount") diff --git a/bimmer_connected/api/client.py b/bimmer_connected/api/client.py index 99b3315c..ab7de8ae 100644 --- a/bimmer_connected/api/client.py +++ b/bimmer_connected/api/client.py @@ -10,7 +10,8 @@ from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.api.regions import get_server_url from bimmer_connected.api.utils import get_correlation_id, log_to_to_file -from bimmer_connected.const import HTTPX_TIMEOUT, USER_AGENT, X_USER_AGENT, CarBrands, Regions +from bimmer_connected.const import HTTPX_TIMEOUT, USER_AGENT, X_USER_AGENT, CarBrands +from bimmer_connected.models import GPSPosition @dataclass @@ -19,6 +20,8 @@ class MyBMWClientConfiguration: authentication: MyBMWAuthentication log_response_path: Optional[pathlib.Path] = None + observer_position: Optional[GPSPosition] = None + use_metric_units: Optional[bool] = True class MyBMWClient(httpx.AsyncClient): @@ -35,7 +38,7 @@ def __init__(self, config: MyBMWClientConfiguration, *args, brand: CarBrands = N # Set default values kwargs["base_url"] = kwargs.get("base_url") or get_server_url(config.authentication.region) - kwargs["headers"] = kwargs.get("headers") or self.generate_default_header(config.authentication.region, brand) + kwargs["headers"] = kwargs.get("headers") or self.generate_default_header(brand) # Register event hooks kwargs["event_hooks"] = defaultdict(list, **kwargs.get("event_hooks", {})) @@ -64,13 +67,13 @@ async def raise_for_status_event_handler(response: httpx.Response): super().__init__(*args, **kwargs) - @staticmethod - def generate_default_header(region: Regions, brand: CarBrands = None) -> Dict[str, str]: + def generate_default_header(self, brand: CarBrands = None) -> Dict[str, str]: """Generate a header for HTTP requests to the server.""" return { "accept": "application/json", "accept-language": "en", "user-agent": USER_AGENT, - "x-user-agent": X_USER_AGENT.format((brand or CarBrands.BMW), region.value), + "x-user-agent": X_USER_AGENT.format((brand or CarBrands.BMW), self.config.authentication.region.value), **get_correlation_id(), + "bmw-units-preferences": "d=KM;v=L" if self.config.use_metric_units else "d=MI;v=G", } diff --git a/bimmer_connected/cli.py b/bimmer_connected/cli.py index 37576140..a6afbba5 100644 --- a/bimmer_connected/cli.py +++ b/bimmer_connected/cli.py @@ -138,7 +138,7 @@ async def fingerprint(args) -> None: for vehicle in account.vehicles: if vehicle.drive_train in HV_BATTERY_DRIVE_TRAINS: print(f"Getting 'charging-sessions' for {vehicle.vin}") - async with MyBMWClient(account.mybmw_client_config, brand=vehicle.brand) as client: + async with MyBMWClient(account.config, brand=vehicle.brand) as client: await client.post( "/eadrax-chs/v1/charging-sessions", params={"vin": vehicle.vin, "maxResults": 40, "include_date_picker": "true"}, diff --git a/bimmer_connected/vehicle/models.py b/bimmer_connected/models.py similarity index 100% rename from bimmer_connected/vehicle/models.py rename to bimmer_connected/models.py diff --git a/bimmer_connected/utils.py b/bimmer_connected/utils.py index 3622f20c..c6bf4a44 100644 --- a/bimmer_connected/utils.py +++ b/bimmer_connected/utils.py @@ -43,7 +43,11 @@ def parse_datetime(date_str: str) -> Optional[datetime.datetime]: try: # Parse datetimes using `time.strptime` to allow running in some embedded python interpreters. # https://bugs.python.org/issue27400 - parsed = datetime.datetime(*(time.strptime(date_str, date_format)[0:6])) + time_struct = time.strptime(date_str, date_format) + parsed = datetime.datetime(*(time_struct[0:6])) + if time_struct.tm_gmtoff and time_struct.tm_gmtoff != 0: + parsed = parsed - datetime.timedelta(seconds=time_struct.tm_gmtoff) + parsed = parsed.replace(tzinfo=datetime.timezone.utc) return parsed except ValueError: pass diff --git a/bimmer_connected/vehicle/charging_profile.py b/bimmer_connected/vehicle/charging_profile.py index 0a72ab9c..97d1b7d3 100644 --- a/bimmer_connected/vehicle/charging_profile.py +++ b/bimmer_connected/vehicle/charging_profile.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional -from bimmer_connected.vehicle.models import StrEnum, VehicleDataBase +from bimmer_connected.models import StrEnum, VehicleDataBase _LOGGER = logging.getLogger(__name__) diff --git a/bimmer_connected/vehicle/doors_windows.py b/bimmer_connected/vehicle/doors_windows.py index e9574b8d..11118a9e 100644 --- a/bimmer_connected/vehicle/doors_windows.py +++ b/bimmer_connected/vehicle/doors_windows.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from typing import Any, Dict, List -from bimmer_connected.vehicle.models import StrEnum, VehicleDataBase +from bimmer_connected.models import StrEnum, VehicleDataBase _LOGGER = logging.getLogger(__name__) diff --git a/bimmer_connected/vehicle/fuel_and_battery.py b/bimmer_connected/vehicle/fuel_and_battery.py index 065fc5a5..f740f0ab 100644 --- a/bimmer_connected/vehicle/fuel_and_battery.py +++ b/bimmer_connected/vehicle/fuel_and_battery.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import Any, Dict, Optional -from bimmer_connected.vehicle.models import StrEnum, ValueWithUnit, VehicleDataBase +from bimmer_connected.models import StrEnum, ValueWithUnit, VehicleDataBase _LOGGER = logging.getLogger(__name__) diff --git a/bimmer_connected/vehicle/location.py b/bimmer_connected/vehicle/location.py index 3802cfb1..577ea4a3 100644 --- a/bimmer_connected/vehicle/location.py +++ b/bimmer_connected/vehicle/location.py @@ -7,8 +7,8 @@ from bimmer_connected.const import Regions from bimmer_connected.coord_convert import gcj2wgs +from bimmer_connected.models import GPSPosition, VehicleDataBase from bimmer_connected.utils import parse_datetime -from bimmer_connected.vehicle.models import GPSPosition, VehicleDataBase _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def from_vehicle_data(cls, vehicle_data: Dict): @classmethod def _parse_vehicle_data(cls, vehicle_data: Dict): - date_dummy = datetime.datetime(1970, 1, 1) + date_dummy = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) retval: Dict[str, Any] = {} retval["vehicle_update_timestamp"] = max( @@ -56,7 +56,9 @@ def _update_after_parse(self, parsed: Dict) -> Dict: retval = parsed # Overwrite vehicle data with remote service position if available & newer if self.remote_service_position is not None: - t_remote = self.remote_service_position.get("timestamp", datetime.datetime(1900, 1, 1)) + t_remote = self.remote_service_position.get( + "timestamp", datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc) + ) if t_remote > self.vehicle_update_timestamp: retval["location"] = GPSPosition( self.remote_service_position["latitude"], self.remote_service_position["longitude"] @@ -77,6 +79,7 @@ def set_remote_service_position(self, remote_service_dict: Dict): else: pos = remote_service_dict["positionData"]["position"] pos["timestamp"] = datetime.datetime.utcnow() + pos["timestamp"] = pos["timestamp"].replace(tzinfo=datetime.timezone.utc) self.remote_service_position = pos diff --git a/bimmer_connected/vehicle/remote_services.py b/bimmer_connected/vehicle/remote_services.py index 188955d6..acf2badc 100644 --- a/bimmer_connected/vehicle/remote_services.py +++ b/bimmer_connected/vehicle/remote_services.py @@ -13,8 +13,8 @@ REMOTE_SERVICE_URL, VEHICLE_POI_URL, ) +from bimmer_connected.models import PointOfInterest, StrEnum from bimmer_connected.utils import MyBMWJSONEncoder -from bimmer_connected.vehicle.models import PointOfInterest, StrEnum if TYPE_CHECKING: from bimmer_connected.vehicle import MyBMWVehicle @@ -150,7 +150,7 @@ async def _start_remote_service(self, service_id: Services, params: Dict = None) """Start a generic remote service.""" url = REMOTE_SERVICE_URL.format(vin=self._vehicle.vin, service_type=service_id.value) - async with MyBMWClient(self._account.mybmw_client_config, brand=self._vehicle.brand) as client: + async with MyBMWClient(self._account.config, brand=self._vehicle.brand) as client: response = await client.post(url, params=params) return response.json().get("eventId") @@ -177,7 +177,7 @@ async def _get_remote_service_status(self, event_id: str = None) -> RemoteServic """The execution status of the last remote service that was triggered.""" _LOGGER.debug("getting remote service status for '%s'", event_id) url = REMOTE_SERVICE_STATUS_URL.format(vin=self._vehicle.vin, event_id=event_id) - async with MyBMWClient(self._account.mybmw_client_config, brand=self._vehicle.brand) as client: + async with MyBMWClient(self._account.config, brand=self._vehicle.brand) as client: response = await client.post(url) return RemoteServiceStatus(response.json()) @@ -199,7 +199,7 @@ async def trigger_send_poi(self, poi: Union[PointOfInterest, Dict]) -> RemoteSer if isinstance(poi, Dict): poi = PointOfInterest(**poi) - async with MyBMWClient(self._account.mybmw_client_config, brand=self._vehicle.brand) as client: + async with MyBMWClient(self._account.config, brand=self._vehicle.brand) as client: await client.post( VEHICLE_POI_URL, headers={"content-type": "application/json"}, @@ -229,19 +229,19 @@ async def trigger_remote_vehicle_finder(self) -> RemoteServiceStatus: async def _get_event_position(self, event_id) -> Dict: url = REMOTE_SERVICE_POSITION_URL.format(event_id=event_id) - if not self._account.observer_position: + if not self._account.config.observer_position: return { "errorDetails": { "title": "Unknown position", "description": "Set observer position to retrieve vehicle coordinates!", } } - async with MyBMWClient(self._account.mybmw_client_config, brand=self._vehicle.brand) as client: + async with MyBMWClient(self._account.config, brand=self._vehicle.brand) as client: response = await client.post( url, headers={ - "latitude": str(self._account.observer_position.latitude), - "longitude": str(self._account.observer_position.longitude), + "latitude": str(self._account.config.observer_position.latitude), + "longitude": str(self._account.config.observer_position.longitude), }, ) return response.json() diff --git a/bimmer_connected/vehicle/reports.py b/bimmer_connected/vehicle/reports.py index d9728a1b..d01ff093 100644 --- a/bimmer_connected/vehicle/reports.py +++ b/bimmer_connected/vehicle/reports.py @@ -5,8 +5,8 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional +from bimmer_connected.models import StrEnum, ValueWithUnit, VehicleDataBase from bimmer_connected.utils import parse_datetime -from bimmer_connected.vehicle.models import StrEnum, ValueWithUnit, VehicleDataBase _LOGGER = logging.getLogger(__name__) diff --git a/bimmer_connected/vehicle/vehicle.py b/bimmer_connected/vehicle/vehicle.py index 7f9d7a04..c1097885 100644 --- a/bimmer_connected/vehicle/vehicle.py +++ b/bimmer_connected/vehicle/vehicle.py @@ -5,19 +5,19 @@ from bimmer_connected.api.client import MyBMWClient from bimmer_connected.const import SERVICE_PROPERTIES, SERVICE_STATUS, VEHICLE_IMAGE_URL, CarBrands +from bimmer_connected.models import StrEnum, ValueWithUnit from bimmer_connected.utils import deprecated, parse_datetime from bimmer_connected.vehicle.charging_profile import ChargingProfile from bimmer_connected.vehicle.doors_windows import DoorsAndWindows from bimmer_connected.vehicle.fuel_and_battery import FuelAndBattery from bimmer_connected.vehicle.location import VehicleLocation -from bimmer_connected.vehicle.models import StrEnum, ValueWithUnit from bimmer_connected.vehicle.remote_services import RemoteServices from bimmer_connected.vehicle.reports import CheckControlMessageReport, ConditionBasedServiceReport from bimmer_connected.vehicle.vehicle_status import VehicleStatus if TYPE_CHECKING: from bimmer_connected.account import MyBMWAccount - from bimmer_connected.vehicle.models import VehicleDataBase + from bimmer_connected.models import VehicleDataBase _LOGGER = logging.getLogger(__name__) @@ -293,7 +293,7 @@ async def get_vehicle_image(self, direction: VehicleViewDirection) -> bytes: view=direction.value, ) # the accept field of the header needs to be updated as we want a png not the usual JSON - async with MyBMWClient(self.account.mybmw_client_config, brand=self.brand) as client: + async with MyBMWClient(self.account.config, brand=self.brand) as client: response = await client.get(url, headers={"accept": "image/png"}) return response.content diff --git a/bimmer_connected/vehicle/vehicle_status.py b/bimmer_connected/vehicle/vehicle_status.py index 3e61ea36..ad62f653 100644 --- a/bimmer_connected/vehicle/vehicle_status.py +++ b/bimmer_connected/vehicle/vehicle_status.py @@ -4,8 +4,8 @@ import logging from typing import TYPE_CHECKING, Dict, List, Optional +from bimmer_connected.models import GPSPosition, ValueWithUnit from bimmer_connected.utils import deprecated -from bimmer_connected.vehicle.models import GPSPosition, ValueWithUnit if TYPE_CHECKING: from bimmer_connected.vehicle import MyBMWVehicle diff --git a/docs/source/module/models.rst b/docs/source/module/models.rst new file mode 100644 index 00000000..166b637b --- /dev/null +++ b/docs/source/module/models.rst @@ -0,0 +1,8 @@ +.. _models_module: + +:mod:`bimmer_connected.models` +====================================== + +.. automodule:: bimmer_connected.models + :members: + :undoc-members: diff --git a/docs/source/module/vehicle.rst b/docs/source/module/vehicle.rst index 86929a30..204abd39 100644 --- a/docs/source/module/vehicle.rst +++ b/docs/source/module/vehicle.rst @@ -53,14 +53,6 @@ The :mod:`bimmer_connected.vehicle` module contains all data & parsers for a veh :undoc-members: -:mod:`bimmer_connected.vehicle.models` --------------------------------------------------------- - -.. automodule:: bimmer_connected.vehicle.models - :members: - :undoc-members: - - :mod:`bimmer_connected.vehicle.reports` -------------------------------------------------------- diff --git a/test/responses/G21/json_export.json b/test/responses/G21/json_export.json index 351f1885..1054e1f7 100644 --- a/test/responses/G21/json_export.json +++ b/test/responses/G21/json_export.json @@ -571,7 +571,7 @@ "longitude": 34.5678 }, "heading": 123, - "vehicle_update_timestamp": "2021-11-14T20:20:21", + "vehicle_update_timestamp": "2021-11-14T20:20:21+00:00", "account_region": "row", "remote_service_position": null }, @@ -647,7 +647,7 @@ { "service_type": "OIL", "state": "OK", - "due_date": "2022-10-01T00:00:00", + "due_date": "2022-10-01T00:00:00+00:00", "due_distance": [ 6000, "KILOMETERS" @@ -656,7 +656,7 @@ { "service_type": "VEHICLE_CHECK", "state": "OK", - "due_date": "2024-10-01T00:00:00", + "due_date": "2024-10-01T00:00:00+00:00", "due_distance": [ 39000, "KILOMETERS" @@ -683,7 +683,7 @@ { "service_type": "BRAKE_FLUID", "state": "OK", - "due_date": "2023-10-01T00:00:00", + "due_date": "2023-10-01T00:00:00+00:00", "due_distance": [ null, null @@ -833,6 +833,6 @@ "km" ], "name": "330e xDrive", - "timestamp": "2021-11-14T20:20:21", + "timestamp": "2021-11-14T20:20:21+00:00", "vin": "some_vin_G21" } \ No newline at end of file diff --git a/test/test_account.py b/test/test_account.py index f931f807..fac2ae95 100644 --- a/test/test_account.py +++ b/test/test_account.py @@ -19,9 +19,10 @@ from bimmer_connected.account import ConnectedDriveAccount, MyBMWAccount from bimmer_connected.api.authentication import MyBMWAuthentication, MyBMWLoginRetry +from bimmer_connected.api.client import MyBMWClient from bimmer_connected.api.regions import get_region_from_name from bimmer_connected.const import Regions -from bimmer_connected.vehicle.models import GPSPosition +from bimmer_connected.models import GPSPosition from . import ( RESPONSE_DIR, @@ -127,14 +128,14 @@ async def test_login_refresh_token_row_na_expired(): with mock.patch( "bimmer_connected.api.authentication.MyBMWAuthentication._refresh_token_row_na", - wraps=account.mybmw_client_config.authentication._refresh_token_row_na, # pylint: disable=protected-access + wraps=account.config.authentication._refresh_token_row_na, # pylint: disable=protected-access ) as mock_listener: mock_listener.reset_mock() await account.get_vehicles() # Should not be called at all, as expiry date is not checked anymore assert mock_listener.call_count == 0 - assert account.mybmw_client_config.authentication.refresh_token is not None + assert account.config.authentication.refresh_token is not None @pytest.mark.asyncio @@ -146,7 +147,7 @@ async def test_login_refresh_token_row_na_401(): with mock.patch( "bimmer_connected.api.authentication.MyBMWAuthentication._refresh_token_row_na", - wraps=account.mybmw_client_config.authentication._refresh_token_row_na, # pylint: disable=protected-access + wraps=account.config.authentication._refresh_token_row_na, # pylint: disable=protected-access ) as mock_listener: mock_api.get("/eadrax-vcs/v1/vehicles").mock( side_effect=[httpx.Response(401), *([httpx.Response(200, json=[])] * 10)] @@ -155,7 +156,7 @@ async def test_login_refresh_token_row_na_401(): await account.get_vehicles() assert mock_listener.call_count == 1 - assert account.mybmw_client_config.authentication.refresh_token is not None + assert account.config.authentication.refresh_token is not None @pytest.mark.asyncio @@ -200,14 +201,14 @@ async def test_login_refresh_token_china_expired(): with mock.patch( "bimmer_connected.api.authentication.MyBMWAuthentication._refresh_token_china", - wraps=account.mybmw_client_config.authentication._refresh_token_china, # pylint: disable=protected-access + wraps=account.config.authentication._refresh_token_china, # pylint: disable=protected-access ) as mock_listener: mock_listener.reset_mock() await account.get_vehicles() # Should not be called at all, as expiry date is not checked anymore assert mock_listener.call_count == 0 - assert account.mybmw_client_config.authentication.refresh_token is not None + assert account.config.authentication.refresh_token is not None @pytest.mark.asyncio @@ -219,7 +220,7 @@ async def test_login_refresh_token_china_401(): with mock.patch( "bimmer_connected.api.authentication.MyBMWAuthentication._refresh_token_china", - wraps=account.mybmw_client_config.authentication._refresh_token_china, # pylint: disable=protected-access + wraps=account.config.authentication._refresh_token_china, # pylint: disable=protected-access ) as mock_listener: mock_api.get("/eadrax-vcs/v1/vehicles").mock( side_effect=[httpx.Response(401), *([httpx.Response(200, json=[])] * 10)] @@ -228,7 +229,7 @@ async def test_login_refresh_token_china_401(): await account.get_vehicles() assert mock_listener.call_count == 1 - assert account.mybmw_client_config.authentication.refresh_token is not None + assert account.config.authentication.refresh_token is not None @pytest.mark.asyncio @@ -261,7 +262,7 @@ async def test_vehicles(): account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name("china")) await account.get_vehicles() - assert account.mybmw_client_config.authentication.access_token is not None + assert account.config.authentication.access_token is not None assert get_fingerprint_count() == len(account.vehicles) vehicle = account.get_vehicle(VIN_G21) @@ -345,7 +346,7 @@ async def test_set_observer_value(): account.set_observer_position(1.0, 2.0) - assert account.observer_position == GPSPosition(1.0, 2.0) + assert account.config.observer_position == GPSPosition(1.0, 2.0) @pytest.mark.asyncio @@ -353,11 +354,11 @@ async def test_set_observer_not_set(): """Test set_observer_position with no arguments.""" account = await get_mocked_account() - assert account.observer_position is None + assert account.config.observer_position is None account.set_observer_position(17.99, 179.9) - assert account.observer_position == GPSPosition(17.99, 179.9) + assert account.config.observer_position == GPSPosition(17.99, 179.9) @pytest.mark.asyncio @@ -375,6 +376,26 @@ async def test_set_observer_invalid_values(): account.set_observer_position(1.0, "16.0") +@pytest.mark.asyncio +async def test_set_use_metric_units(): + """Test set_observer_position with no arguments.""" + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + assert account.config.use_metric_units is True + + metric_client = MyBMWClient(account.config) + assert metric_client.generate_default_header()["bmw-units-preferences"] == "d=KM;v=L" + + account.set_use_metric_units(False) + assert account.config.use_metric_units is False + imperial_client = MyBMWClient(account.config) + assert imperial_client.generate_default_header()["bmw-units-preferences"] == "d=MI;v=G" + + imperial_account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, use_metric_units=False) + assert imperial_account.config.use_metric_units is False + imperial_client = MyBMWClient(imperial_account.config) + assert imperial_client.generate_default_header()["bmw-units-preferences"] == "d=MI;v=G" + + @account_mock() @pytest.mark.asyncio async def test_deprecated_account(caplog): diff --git a/test/test_deprecated_vehicle_status.py b/test/test_deprecated_vehicle_status.py index b0dda8be..f601820f 100644 --- a/test/test_deprecated_vehicle_status.py +++ b/test/test_deprecated_vehicle_status.py @@ -19,7 +19,9 @@ async def test_generic(caplog): """Test generic attributes.""" status = (await get_mocked_account()).get_vehicle(VIN_G30).status - expected = datetime.datetime(year=2021, month=11, day=11, hour=8, minute=58, second=53) + expected = datetime.datetime( + year=2021, month=11, day=11, hour=8, minute=58, second=53, tzinfo=datetime.timezone.utc + ) assert expected == status.timestamp assert 7991 == status.mileage[0] @@ -204,17 +206,17 @@ async def test_condition_based_services(caplog): cbs = status.condition_based_services assert 3 == len(cbs) assert ConditionBasedServiceStatus.OK == cbs[0].state - expected_cbs0 = datetime.datetime(year=2022, month=8, day=1) + expected_cbs0 = datetime.datetime(year=2022, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs0 == cbs[0].due_date assert (25000, "KILOMETERS") == cbs[0].due_distance assert ConditionBasedServiceStatus.OK == cbs[1].state - expected_cbs1 = datetime.datetime(year=2023, month=8, day=1) + expected_cbs1 = datetime.datetime(year=2023, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs1 == cbs[1].due_date assert (None, None) == cbs[1].due_distance assert ConditionBasedServiceStatus.OK == cbs[2].state - expected_cbs2 = datetime.datetime(year=2024, month=8, day=1) + expected_cbs2 = datetime.datetime(year=2024, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs2 == cbs[2].due_date assert (60000, "KILOMETERS") == cbs[2].due_distance diff --git a/test/test_utils.py b/test/test_utils.py index 47deb333..97173978 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -2,6 +2,7 @@ import datetime import json import os +import sys import time from unittest import mock @@ -76,12 +77,16 @@ async def test_to_json(caplog): def test_parse_datetime(caplog): """Test datetime parser.""" - dt_without_milliseconds = datetime.datetime(2021, 11, 12, 13, 14, 15) + dt_without_milliseconds = datetime.datetime(2021, 11, 12, 13, 14, 15, tzinfo=datetime.timezone.utc) assert dt_without_milliseconds == parse_datetime("2021-11-12T13:14:15.567Z") assert dt_without_milliseconds == parse_datetime("2021-11-12T13:14:15Z") + if sys.version_info >= (3, 7): + # Don't test timezone parsing on Python 3.6 (not supported there) + assert dt_without_milliseconds == parse_datetime("2021-11-12T16:14:15+03:00") + unparseable_datetime = "2021-14-12T13:14:15Z" assert parse_datetime(unparseable_datetime) is None errors = [r for r in caplog.records if r.levelname == "ERROR" and unparseable_datetime in r.message] diff --git a/test/test_vehicle.py b/test/test_vehicle.py index c5c85f0f..e2aebd54 100644 --- a/test/test_vehicle.py +++ b/test/test_vehicle.py @@ -2,8 +2,8 @@ import pytest from bimmer_connected.const import CarBrands +from bimmer_connected.models import GPSPosition, StrEnum, VehicleDataBase from bimmer_connected.vehicle import DriveTrainType, VehicleViewDirection -from bimmer_connected.vehicle.models import GPSPosition, StrEnum, VehicleDataBase from . import ( VIN_F11, diff --git a/test/test_vehicle_status.py b/test/test_vehicle_status.py index 20afb9c0..71e2279c 100644 --- a/test/test_vehicle_status.py +++ b/test/test_vehicle_status.py @@ -20,7 +20,9 @@ async def test_generic(caplog): """Test generic attributes.""" status = (await get_mocked_account()).get_vehicle(VIN_G30) - expected = datetime.datetime(year=2021, month=11, day=11, hour=8, minute=58, second=53) + expected = datetime.datetime( + year=2021, month=11, day=11, hour=8, minute=58, second=53, tzinfo=datetime.timezone.utc + ) assert expected == status.timestamp assert 7991 == status.mileage[0] @@ -205,17 +207,17 @@ async def test_condition_based_services(caplog): cbs = vehicle.condition_based_services.messages assert 3 == len(cbs) assert ConditionBasedServiceStatus.OK == cbs[0].state - expected_cbs0 = datetime.datetime(year=2022, month=8, day=1) + expected_cbs0 = datetime.datetime(year=2022, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs0 == cbs[0].due_date assert (25000, "KILOMETERS") == cbs[0].due_distance assert ConditionBasedServiceStatus.OK == cbs[1].state - expected_cbs1 = datetime.datetime(year=2023, month=8, day=1) + expected_cbs1 = datetime.datetime(year=2023, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs1 == cbs[1].due_date assert (None, None) == cbs[1].due_distance assert ConditionBasedServiceStatus.OK == cbs[2].state - expected_cbs2 = datetime.datetime(year=2024, month=8, day=1) + expected_cbs2 = datetime.datetime(year=2024, month=8, day=1, tzinfo=datetime.timezone.utc) assert expected_cbs2 == cbs[2].due_date assert (60000, "KILOMETERS") == cbs[2].due_distance