1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
|
"""Generals models used for bimmer_connected."""
import logging
from dataclasses import InitVar, dataclass, field
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple, Union
from bimmer_connected.const import DEFAULT_POI_NAME
_LOGGER = logging.getLogger(__name__)
class StrEnum(str, Enum):
"""A string enumeration of type `(str, Enum)`. All members are compared via `upper()`. Defaults to UNKNOWN."""
@classmethod
def _missing_(cls, value):
has_unknown = False
for member in cls:
if member.value.upper() == "UNKNOWN":
has_unknown = True
if member.value.upper() == value.upper():
return member
if has_unknown:
_LOGGER.warning("'%s' is not a valid '%s'", value, cls.__name__)
return cls.UNKNOWN
raise ValueError(f"'{value}' is not a valid {cls.__name__}")
@dataclass
class VehicleDataBase:
"""A base class for parsing and storing complex vehicle data."""
@classmethod
def from_vehicle_data(cls, vehicle_data: Dict):
"""Create the class based on vehicle data from API."""
parsed = cls._parse_vehicle_data(vehicle_data) or {}
if len(parsed) > 0:
return cls(**parsed)
return None
def update_from_vehicle_data(self, vehicle_data: Dict):
"""Update the attributes based on vehicle data from API."""
parsed = self._parse_vehicle_data(vehicle_data) or {}
parsed.update(self._update_after_parse(parsed))
if len(parsed) > 0:
self.__dict__.update(parsed)
@classmethod
def _parse_vehicle_data(cls, vehicle_data: Dict) -> Optional[Dict]:
"""Parse desired attributes out of vehicle data from API."""
raise NotImplementedError()
def _update_after_parse(self, parsed: Dict) -> Dict:
"""Update parsed vehicle data with attributes stored in class if needed."""
return parsed
@dataclass
class GPSPosition:
"""GPS coordinates."""
latitude: Optional[float]
longitude: Optional[float]
def __post_init__(self):
non_null_values = [k for k, v in self.__dict__.items() if v is None]
if len(non_null_values) not in [0, len(self.__dataclass_fields__)]:
raise TypeError(f"{type(self).__name__} requires either none or both arguments set")
for field_name in self.__dataclass_fields__:
value = getattr(self, field_name)
if value is not None and not isinstance(value, (float, int)):
raise TypeError(f"{type(self).__name__} '{field_name}' not of type '{Optional[Union[float, int]]}'")
if field_name == "latitude" and not (-90 <= value <= 90):
raise ValueError(f"{type(self).__name__} 'latitude' must be between -90 and 90, but got '{value}'")
elif field_name == "longitude" and not (-180 <= value <= 180):
raise ValueError(f"{type(self).__name__} 'longitude' must be between -180 and 180, but got '{value}'")
# Force conversion to float
setattr(self, field_name, float(value))
@classmethod
def init_nonempty(cls, latitude: Optional[float], longitude: Optional[float]):
"""Initialize GPSPosition but do not allow empty latitude/longitude."""
if latitude is None or longitude is None:
raise ValueError(f"{cls.__name__} requires both 'latitude' and 'longitude' set")
return cls(latitude, longitude)
def __iter__(self):
yield from self.__dict__.values()
def __getitem__(self, key):
return tuple(self.__dict__.values())[key]
def __eq__(self, other):
if isinstance(other, Tuple):
return tuple(self.__iter__()) == other
if hasattr(self, "__dict__") and hasattr(other, "__dict__"):
return self.__dict__ == other.__dict__
if hasattr(self, "__dict__") and isinstance(other, Dict):
return self.__dict__ == other
return False
@dataclass
class PointOfInterestAddress:
"""Address data of a PointOfInterest."""
street: Optional[str] = None
postalCode: Optional[str] = None
city: Optional[str] = None
country: Optional[str] = None
formatted: Optional[str] = None
# The following attributes are not by us but available in the API
banchi: Optional[str] = None
chome: Optional[str] = None
countryCode: Optional[str] = None
district: Optional[str] = None
go: Optional[str] = None
houseNumber: Optional[str] = None
region: Optional[str] = None
regionCode: Optional[str] = None
settlement: Optional[str] = None
subblock: Optional[str] = None
@dataclass
class PointOfInterest:
"""A Point of Interest to be sent to the car."""
lat: InitVar[float]
lon: InitVar[float]
name: InitVar[Optional[str]] = DEFAULT_POI_NAME
street: InitVar[str] = None
postal_code: InitVar[str] = None
city: InitVar[str] = None
country: InitVar[str] = None
position: Dict[str, float] = field(init=False)
address: Optional[PointOfInterestAddress] = field(init=False)
# The following attributes are not by us but required in the API
formattedAddress: Optional[str] = None
entrances: Optional[List] = field(init=False)
placeType: Optional[str] = "ADDRESS"
category: Dict[str, Optional[str]] = field(init=False)
title: Optional[str] = DEFAULT_POI_NAME
# The following attributes are not by us but available in the API
provider: Optional[str] = None
providerId: Optional[str] = None
providerPoiId: str = ""
sourceType: Optional[str] = None
vehicleCategoryId: Optional[str] = None
def __post_init__(self, lat, lon, name, street, postal_code, city, country):
position = GPSPosition(lat, lon)
self.position = {
"lat": position.latitude,
"lon": position.longitude,
}
self.address = PointOfInterestAddress(str(street), str(postal_code), str(city), str(country))
self.category = {"losCategory": "Address", "mguVehicleCategoryId": None, "name": "Address"}
self.title = name
if not self.formattedAddress:
self.formattedAddress = ", ".join([str(i) for i in [street, postal_code, city] if i]) or "Coordinates only"
class ValueWithUnit(NamedTuple):
"""A value with a corresponding unit."""
value: Optional[Union[int, float]]
unit: Optional[str]
@dataclass
class AnonymizedResponse:
"""An anonymized response."""
filename: str
content: Optional[Union[List, Dict, str]] = None
@dataclass
class ChargingSettings:
"""Charging settings to control the vehicle."""
chargingTarget: Optional[int]
isUnlockCableActive = None
acLimitValue: Optional[int] = None
dcLoudness = None
class MyBMWAPIError(Exception):
"""General BMW API error."""
class MyBMWAuthError(MyBMWAPIError):
"""Auth-related error from BMW API (HTTP status codes 401 and 403)."""
class MyBMWQuotaError(MyBMWAPIError):
"""Quota exceeded on BMW API."""
class MyBMWRemoteServiceError(MyBMWAPIError):
"""Error when executing remote services."""
|