Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bmw-unit-preferences #449

Merged
merged 6 commits into from
May 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions bimmer_connected/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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),
Expand All @@ -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
]
Expand Down Expand Up @@ -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):
Expand All @@ -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")
Expand Down
13 changes: 8 additions & 5 deletions bimmer_connected/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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", {}))
Expand Down Expand Up @@ -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",
}
2 changes: 1 addition & 1 deletion bimmer_connected/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
File renamed without changes.
6 changes: 5 additions & 1 deletion bimmer_connected/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/charging_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/doors_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/fuel_and_battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
9 changes: 6 additions & 3 deletions bimmer_connected/vehicle/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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(
Expand All @@ -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"]
Expand All @@ -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

Expand Down
16 changes: 8 additions & 8 deletions bimmer_connected/vehicle/remote_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand All @@ -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())

Expand All @@ -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"},
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
6 changes: 3 additions & 3 deletions bimmer_connected/vehicle/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/vehicle_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/source/module/models.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _models_module:

:mod:`bimmer_connected.models`
======================================

.. automodule:: bimmer_connected.models
:members:
:undoc-members:
8 changes: 0 additions & 8 deletions docs/source/module/vehicle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
--------------------------------------------------------

Expand Down
Loading