diff --git a/custom_components/polestar_api/__init__.py b/custom_components/polestar_api/__init__.py index ed84dd5..9fb9316 100644 --- a/custom_components/polestar_api/__init__.py +++ b/custom_components/polestar_api/__init__.py @@ -41,6 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass, conf[CONF_USERNAME], conf[CONF_PASSWORD]) try: await polestarApi.init() + polestarApi.set_config_unit(hass.config.units) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = polestarApi diff --git a/custom_components/polestar_api/polestar.py b/custom_components/polestar_api/polestar.py index cc0c9b4..7e3d878 100644 --- a/custom_components/polestar_api/polestar.py +++ b/custom_components/polestar_api/polestar.py @@ -6,6 +6,7 @@ from urllib3 import disable_warnings from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM, UnitSystem from .pypolestar.exception import PolestarApiException, PolestarAuthException from .pypolestar.polestar import PolestarApi @@ -25,6 +26,7 @@ def __init__(self, self.name = "Polestar " self.polestarApi = PolestarApi(username, password) self.vin = None + self.unit_system = METRIC_SYSTEM disable_warnings() async def init(self): @@ -71,6 +73,11 @@ async def async_update(self) -> None: _LOGGER.error("Unexpected Error on update data %s", str(e)) self.polestarApi.next_update = datetime.now() + timedelta(seconds=60) + def set_config_unit(self, unit:UnitSystem): + self.unit_system = unit + + def get_config_unit(self): + return self.unit_system def get_value(self, query: str, field_name: str, skip_cache: bool = False): data = self.polestarApi.get_cache_data(query, field_name, skip_cache) diff --git a/custom_components/polestar_api/pypolestar/auth.py b/custom_components/polestar_api/pypolestar/auth.py index 0766c37..a6bdf6f 100644 --- a/custom_components/polestar_api/pypolestar/auth.py +++ b/custom_components/polestar_api/pypolestar/auth.py @@ -4,6 +4,7 @@ import httpx +from .const import HTTPX_TIMEOUT from .exception import PolestarAuthException _LOGGER = logging.getLogger(__name__) @@ -46,7 +47,7 @@ async def get_token(self, refresh=False) -> None: "operationName": operationName, "variables": json.dumps({"token": token}), } - result = await self._client_session.get("https://pc-api.polestar.com/eu-north-1/auth/", params=params, headers=headers) + result = await self._client_session.get("https://pc-api.polestar.com/eu-north-1/auth/", params=params, headers=headers, timeout=HTTPX_TIMEOUT) self.latest_call_code = result.status_code resultData = result.json() if result.status_code != 200 or ("errors" in resultData and len(resultData["errors"])): @@ -97,7 +98,7 @@ async def _get_code(self) -> None: code = result.next_request.url.params.get('code') # sign-in-callback - result = await self._client_session.get(result.next_request.url) + result = await self._client_session.get(result.next_request.url, timeout=HTTPX_TIMEOUT) self.latest_call_code = result.status_code if result.status_code != 200: @@ -116,7 +117,7 @@ async def _get_resume_path(self): "client_id": "polmystar", "redirect_uri": "https://www.polestar.com/sign-in-callback" } - result = await self._client_session.get("https://polestarid.eu.polestar.com/as/authorization.oauth2", params=params) + result = await self._client_session.get("https://polestarid.eu.polestar.com/as/authorization.oauth2", params=params, timeout=HTTPX_TIMEOUT) if result.status_code != 303: raise PolestarAuthException("Error getting resume path ", result.status_code) return result.next_request.url.params diff --git a/custom_components/polestar_api/pypolestar/const.py b/custom_components/polestar_api/pypolestar/const.py index f807541..b72fafe 100644 --- a/custom_components/polestar_api/pypolestar/const.py +++ b/custom_components/polestar_api/pypolestar/const.py @@ -3,3 +3,5 @@ CAR_INFO_DATA = "getConsumerCarsV2" ODO_METER_DATA = "getOdometerData" BATTERY_DATA = "getBatteryData" + +HTTPX_TIMEOUT = 30 diff --git a/custom_components/polestar_api/sensor.py b/custom_components/polestar_api/sensor.py index c26acea..1ba7eca 100644 --- a/custom_components/polestar_api/sensor.py +++ b/custom_components/polestar_api/sensor.py @@ -22,6 +22,7 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util.unit_system import METRIC_SYSTEM from . import DOMAIN as POLESTAR_API_DOMAIN from .entity import PolestarEntity @@ -83,31 +84,32 @@ class PolestarSensorDescription( POLESTAR_SENSOR_TYPES: Final[tuple[PolestarSensorDescription, ...]] = ( PolestarSensorDescription( - key="estimate_distance_to_empty_miles", - name="Distance Miles Remaining", - icon="mdi:map-marker-distance", - query="getBatteryData", - field_name="estimatedDistanceToEmptyMiles", - unit=UnitOfLength.MILES, - round_digits=None, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.DISTANCE, - max_value=410, - dict_data=None, - ), - PolestarSensorDescription( - key="estimate_distance_to_empty_km", - name="Distance Km Remaining", + key="estimate_range", + name="Range", icon="mdi:map-marker-distance", query="getBatteryData", field_name="estimatedDistanceToEmptyKm", unit=UnitOfLength.KILOMETERS, - round_digits=None, + round_digits=2, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, - max_value=660, # WLTP range max 655 + max_value=660, dict_data=None ), + # deprecated +# PolestarSensorDescription( +# key="estimate_distance_to_empty_miles", +# name="Distance Miles Remaining", +# icon="mdi:map-marker-distance", +# query="getBatteryData", +# field_name="estimatedDistanceToEmptyMiles", +# unit=UnitOfLength.MILES, +# round_digits=None, +# state_class=SensorStateClass.MEASUREMENT, +# device_class=SensorDeviceClass.DISTANCE, +# max_value=410, +# dict_data=None, +# ), PolestarSensorDescription( key="current_odometer_meters", name="Odometer", @@ -122,7 +124,7 @@ class PolestarSensorDescription( dict_data=None ), PolestarSensorDescription( - key="average_speed_km_per_hour", + key="average_speed_per_hour", name="Avg. Speed", icon="mdi:speedometer", query="getOdometerData", @@ -234,7 +236,7 @@ class PolestarSensorDescription( dict_data=CHARGING_CONNECTION_STATUS_DICT ), PolestarSensorDescription( - key="average_energy_consumption_kwh_per_100_km", + key="average_energy_consumption_kwh_per_100", name="Avg. Energy Consumption", icon="mdi:battery-clock", query="getBatteryData", @@ -331,22 +333,10 @@ class PolestarSensorDescription( max_value=None, dict_data=None ), - PolestarSensorDescription( - key="estimate_full_charge_range_miles", - name="Calc. Miles Full Charge", - icon="mdi:map-marker-distance", - query="getBatteryData", - field_name="estimatedDistanceToEmptyMiles", - unit=UnitOfLength.MILES, - round_digits=None, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.DISTANCE, - max_value=410, - dict_data=None - ), + PolestarSensorDescription( key="estimate_full_charge_range", - name="Calc. Km Full Charge", + name="Calc. Full Charge Range", icon="mdi:map-marker-distance", query="getBatteryData", field_name="estimatedDistanceToEmptyKm", @@ -497,6 +487,26 @@ def state(self) -> StateType: self._attr_native_value = int( self._attr_native_value.replace('.0', '')) + is_metric = self._device.get_config_unit() == METRIC_SYSTEM + if self.entity_description.key in ('estimate_full_charge_range', 'estimate_range', 'current_trip_meter_manual', 'current_trip_meter_automatic', 'current_odometer_meters', 'average_speed_per_hour', 'average_energy_consumption_kwh_per_100'): + if self.entity_description.key == "average_speed_per_hour": + self.entity_description.unit = UnitOfSpeed.KILOMETERS_PER_HOUR if is_metric else UnitOfSpeed.MILES_PER_HOUR + elif self.entity_description.key == "average_energy_consumption_kwh_per_100": + self.entity_description.unit = 'kWh/100km' if is_metric else 'kWh/100mi' + else: + self.entity_description.unit = UnitOfLength.KILOMETERS if is_metric else UnitOfLength.MILES + + if not is_metric: + # todo: need to check if current_trip should normally in KM + # todo: check if we need to convert current_odo_meter to miles + self._attr_native_value = self._attr_native_value * 0.621371 + + if self.entity_description.key == "estimate_range": + if not is_metric: + self.entity_description.max_value = 410 + else: + self.entity_description.max_value = 660 + # prevent exponentianal value, we only give state value that is lower than the max value if self.entity_description.max_value is not None: if isinstance(self._attr_native_value, str): @@ -511,7 +521,7 @@ def state(self) -> StateType: return datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=round(value)) return 'Not charging' - if self.entity_description.key in ('estimate_full_charge_range', 'estimate_full_charge_range_miles'): + if self.entity_description.key in ('estimate_full_charge_range'): battery_level = self._device.get_latest_data( self.entity_description.query, 'batteryChargeLevelPercentage') estimate_range = self._device.get_latest_data( @@ -530,6 +540,8 @@ def state(self) -> StateType: return estimate_range + + if self.entity_description.key in ('current_odometer_meters'): if int(self._attr_native_value) > 1000: km = self._attr_native_value / 1000