diff --git a/custom_components/polestar_api/__init__.py b/custom_components/polestar_api/__init__.py index ef00591..66154ea 100644 --- a/custom_components/polestar_api/__init__.py +++ b/custom_components/polestar_api/__init__.py @@ -6,6 +6,7 @@ from aiohttp import ClientConnectionError from async_timeout import timeout + from .polestar_api import PolestarApi from homeassistant.config_entries import ConfigEntry @@ -20,7 +21,6 @@ from .const import ( CONF_VIN, - CONF_VCC_API_KEY, DOMAIN, TIMEOUT ) @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("async_setup_entry: %s", config_entry) polestarApi = PolestarApi( - hass, conf[CONF_USERNAME], conf[CONF_PASSWORD], conf[CONF_VIN], conf[CONF_VCC_API_KEY]) + hass, conf[CONF_USERNAME], conf[CONF_PASSWORD], conf[CONF_VIN]) await polestarApi.init() hass.data.setdefault(DOMAIN, {}) diff --git a/custom_components/polestar_api/config_flow.py b/custom_components/polestar_api/config_flow.py index a576a92..a4e0c68 100644 --- a/custom_components/polestar_api/config_flow.py +++ b/custom_components/polestar_api/config_flow.py @@ -11,7 +11,7 @@ from .polestar import PolestarApi -from .const import CONF_VIN, CONF_VCC_API_KEY, DOMAIN, TIMEOUT +from .const import CONF_VIN, DOMAIN, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class FlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - async def _create_entry(self, username: str, password: str, vin: str, vcc_api_key: str) -> None: + async def _create_entry(self, username: str, password: str, vin: str) -> None: """Register new entry.""" return self.async_create_entry( title='Polestar EV', @@ -31,11 +31,10 @@ async def _create_entry(self, username: str, password: str, vin: str, vcc_api_ke CONF_USERNAME: username, CONF_PASSWORD: password, CONF_VIN: vin, - CONF_VCC_API_KEY: vcc_api_key } ) - async def _create_device(self, username: str, password: str, vin: str, vcc_api_key: str) -> None: + async def _create_device(self, username: str, password: str, vin: str) -> None: """Create device.""" try: @@ -43,9 +42,7 @@ async def _create_device(self, username: str, password: str, vin: str, vcc_api_k self.hass, username, password, - vin, - vcc_api_key - ) + vin) with timeout(TIMEOUT): await device.init() @@ -64,7 +61,7 @@ async def _create_device(self, username: str, password: str, vin: str, vcc_api_k _LOGGER.exception("Unexpected error creating device") return self.async_abort(reason="api_failed") - return await self._create_entry(username, password, vin, vcc_api_key) + return await self._create_entry(username, password, vin) async def async_step_user(self, user_input: dict = None) -> None: """User initiated config flow.""" @@ -73,12 +70,11 @@ async def async_step_user(self, user_input: dict = None) -> None: step_id="user", data_schema=vol.Schema({ vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_VIN): str, - vol.Required(CONF_VCC_API_KEY): str, + vol.Required(CONF_VIN): str }) ) - return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_VIN], user_input[CONF_VCC_API_KEY]) + return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_VIN]) async def async_step_import(self, user_input: dict) -> None: """Import a config entry.""" - return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_VIN], user_input[CONF_VCC_API_KEY]) + return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_VIN]) diff --git a/custom_components/polestar_api/const.py b/custom_components/polestar_api/const.py index dc41230..42d0bc7 100644 --- a/custom_components/polestar_api/const.py +++ b/custom_components/polestar_api/const.py @@ -3,13 +3,11 @@ CONF_VIN = "vin" -CONF_VCC_API_KEY = "vcc_api_key" ACCESS_TOKEN_MANAGER_ID = "JWTh4Yf0b" GRANT_TYPE = "password" AUTHORIZATION = "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=" HEADER_AUTHORIZATION = "authorization" -HEADER_VCC_API_KEY = "vcc-api-key" -CACHE_TIME = 10 +CACHE_TIME = 30 diff --git a/custom_components/polestar_api/polestar_api.py b/custom_components/polestar_api/polestar_api.py index 29912cc..78e7c5f 100644 --- a/custom_components/polestar_api/polestar_api.py +++ b/custom_components/polestar_api/polestar_api.py @@ -1,13 +1,13 @@ from datetime import datetime, timedelta import json import logging + from .const import ( ACCESS_TOKEN_MANAGER_ID, AUTHORIZATION, CACHE_TIME, GRANT_TYPE, HEADER_AUTHORIZATION, - HEADER_VCC_API_KEY ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +16,6 @@ from homeassistant.core import HomeAssistant - POST_HEADER_JSON = {"Content-Type": "application/json"} _LOGGER = logging.getLogger(__name__) @@ -28,7 +27,6 @@ def __init__(self, username: str, password: str, vin: str, - vcc_api_key: str, ) -> None: self.id = vin[:8] self.name = "Polestar " + vin[-4:] @@ -39,38 +37,95 @@ def __init__(self, self.token_type = None self.refresh_token = None self.vin = vin - self.vcc_api_key = vcc_api_key - self.cache_data = None + self.cache_data = {} self.latest_call_code = None + self.updating = False disable_warnings() async def init(self): await self.get_token() + result = await self.get_vehicle_data() + self.cache_data['getConsumerCarsV2'] = { + 'data': result['data']['getConsumerCarsV2'][0], 'timestamp': datetime.now()} + + async def _get_resume_path(self): + # Get Resume Path + params = { + "response_type": "code", + "client_id": "polmystar", + "redirect_uri": "https://www.polestar.com/sign-in-callback" + } + result = await self._session.get("https://polestarid.eu.polestar.com/as/authorization.oauth2", params=params) + if result.status != 200: + _LOGGER.error(f"Error getting resume path {result.status}") + return + return result.real_url.query_string + + async def _get_code(self) -> None: + resumePath = await self._get_resume_path() + + # if resumepath has code, then we don't need to login + if 'code' in resumePath: + return resumePath.replace('code=', '') + + # if resumepath has resumepat + + # get the realUrl + resumePath = resumePath.replace( + 'resumePath=', '').replace('&client_id=polmystar', '') + + if resumePath is None: + return + + params = { + 'client_id': 'polmystar' + } + data = { + 'pf.username': self.username, + 'pf.pass': self.password + } + result = await self._session.post(f"https://polestarid.eu.polestar.com/as/{resumePath}/resume/as/authorization.ping", params=params, data=data) + if result.status != 200: + _LOGGER.error(f"Error getting code {result.status}") + return + # get the realUrl + url = result.url + code = result.url.query_string.replace('code=', '') + + # sign-in-callback + result = await self._session.get("https://www.polestar.com/sign-in-callback?code=" + code) + if result.status != 200: + _LOGGER.error(f"Error getting code callback {result.status}") + return + # url encode the code + result = await self._session.get(url) + + return code async def get_token(self) -> None: - response = await self._session.post( - url='https://volvoid.eu.volvocars.com/as/token.oauth2', - data={ - 'username': self.username, - 'password': self.password, - 'grant_type': GRANT_TYPE, - 'access_token_manager_id': ACCESS_TOKEN_MANAGER_ID, - 'scope': 'openid email profile care_by_volvo:financial_information:invoice:read care_by_volvo:financial_information:payment_method care_by_volvo:subscription:read customer:attributes customer:attributes:write order:attributes vehicle:attributes tsp_customer_api:all conve:brake_status conve:climatization_start_stop conve:command_accessibility conve:commands conve:diagnostics_engine_status conve:diagnostics_workshop conve:doors_status conve:engine_status conve:environment conve:fuel_status conve:honk_flash conve:lock conve:lock_status conve:navigation conve:odometer_status conve:trip_statistics conve:tyre_status conve:unlock conve:vehicle_relation conve:warnings conve:windows_status energy:battery_charge_level energy:charging_connection_status energy:charging_system_status energy:electric_range energy:estimated_charging_time energy:recharge_status' - }, - headers={ - HEADER_AUTHORIZATION: AUTHORIZATION, - 'content-type': 'application/x-www-form-urlencoded', - 'user-agent': 'okhttp/4.10.0' - }, - ) - _LOGGER.debug(f"Response {response}") - if response.status != 200: - _LOGGER.info("Info API not available") + code = await self._get_code() + if code is None: return - resp = await response.json(content_type=None) - self.access_token = resp['access_token'] - self.refresh_token = resp['refresh_token'] - self.token_type = resp['token_type'] + + # get token + params = { + "query": "query getAuthToken($code: String!) { getAuthToken(code: $code) { id_token access_token refresh_token expires_in }}", + "operationName": "getAuthToken", + "variables": "{\"code\":\"" + code + "\"}" + } + headers = { + "Content-Type": "application/json" + } + result = await self._session.get("https://pc-api.polestar.com/eu-north-1/auth/", params=params, headers=headers) + if result.status != 200: + _LOGGER.error(f"Error getting code {result.status}") + return + resultData = await result.json() + _LOGGER.debug(resultData) + + self.access_token = resultData['data']['getAuthToken']['access_token'] + self.refresh_token = resultData['data']['getAuthToken']['refresh_token'] + # ID Token _LOGGER.debug(f"Response {self.access_token}") @@ -88,72 +143,94 @@ def get_latest_data(self, path: str, reponse_path: str = None) -> dict or bool o data = data[key] return data - def get_cache_data(self, path: str, reponse_path: str = None) -> dict or bool or None: - # replace the string {vin} with the actual vin - if path is None: - return None - path = path.replace('{vin}', self.vin) - - if self.cache_data and self.cache_data[path]: - if self.cache_data[path]['timestamp'] > datetime.now() - timedelta(seconds=CACHE_TIME): - data = self.cache_data[path]['data'] - if data is None: - return False - if reponse_path: - for key in reponse_path.split('.'): - data = data[key] + def _get_field_name_value(self, field_name: str, data: dict) -> str or bool or None: + if '/' in field_name: + field_name = field_name.split('/') + if data: + if isinstance(field_name, list): + for key in field_name: + data = data[key] return data - - async def get_data(self, path: str, reponse_path: str = None) -> dict or bool or None: - if path is None: - return None - - path = path.replace('{vin}', self.vin) - - cache_data = self.get_cache_data(path, reponse_path) - # if false, then we are fetching data just return - if cache_data is False: + return data[field_name] + return None + + def get_cache_data(self, query: str, field_name: str, skip_cache: bool = False): + if self.cache_data and self.cache_data[query]: + if skip_cache is False: + if self.cache_data[query]['timestamp'] + timedelta(seconds=CACHE_TIME) > datetime.now(): + data = self.cache_data[query]['data'] + if data is None: + return None + return self._get_field_name_value(field_name, data) + else: + data = self.cache_data[query]['data'] + if data is None: + return None + return self._get_field_name_value(field_name, data) + return None + + async def getOdometerData(self): + result = await self.get_odo_data() + # put result in cache + self.cache_data['getOdometerData'] = { + 'data': result['data']['getOdometerData'], 'timestamp': datetime.now()} + + async def getBatteryData(self): + result = await self.get_battery_data() + # put result in cache + self.cache_data['getBatteryData'] = { + 'data': result['data']['getBatteryData'], 'timestamp': datetime.now()} + + async def get_ev_data(self): + if self.updating is True: return - if cache_data: - _LOGGER.debug("Using cached data") - return cache_data - - # put as fast possible something in the cache otherwise we get a lot of requests - if not self.cache_data: - self.cache_data = {} - self.cache_data[path] = {'data': None, 'timestamp': datetime.now()} - else: - self.cache_data[path]['timestamp'] = datetime.now() - - url = 'https://api.volvocars.com/energy/v1/vehicles/' + path + self.updating = True + await self.getOdometerData() + await self.getBatteryData() + self.updating = False + + async def get_graph_ql(self, params: dict): headers = { - HEADER_AUTHORIZATION: f'{self.token_type} {self.access_token}', - HEADER_VCC_API_KEY: self.vcc_api_key + "Content-Type": "application/json", + "authorization": "Bearer " + self.access_token } - response = await self._session.get( - url=url, - headers=headers - ) - _LOGGER.debug(f"Response {response}") - self.latest_call_code = response.status - if response.status == 401: - await self.get_token() - return - if response.status != 200: - _LOGGER.debug("Info API not available") - return - resp = await response.json(content_type=None) - - _LOGGER.debug(f"Response {resp}") - - data = resp['data'] - - # add cache_data[path] - self.cache_data[path] = {'data': data, 'timestamp': datetime.now()} - - if reponse_path: - for key in reponse_path.split('.'): - data = data[key] - - return data + result = await self._session.get("https://pc-api.polestar.com/eu-north-1/my-star/", params=params, headers=headers) + resultData = await result.json() + + # if auth error, get new token + if resultData.get('errors'): + if resultData['errors'][0]['message'] == 'User not authenticated': + await self.get_token() + resultData = await self.get_graph_ql(params) + # log the error + _LOGGER.info(resultData.get('errors')) + _LOGGER.debug(resultData) + return resultData + + async def get_odo_data(self): + # get Odo Data + params = { + "query": "query GetOdometerData($vin: String!) { getOdometerData(vin: $vin) { averageSpeedKmPerHour eventUpdatedTimestamp { iso unix __typename } odometerMeters tripMeterAutomaticKm tripMeterManualKm __typename }}", + "operationName": "GetOdometerData", + "variables": "{\"vin\":\"" + self.vin + "\"}" + } + return await self.get_graph_ql(params) + + async def get_battery_data(self): + # get Battery Data + params = { + "query": "query GetBatteryData($vin: String!) { getBatteryData(vin: $vin) { averageEnergyConsumptionKwhPer100Km batteryChargeLevelPercentage chargerConnectionStatus chargingCurrentAmps chargingPowerWatts chargingStatus estimatedChargingTimeMinutesToTargetDistance estimatedChargingTimeToFullMinutes estimatedDistanceToEmptyKm estimatedDistanceToEmptyMiles eventUpdatedTimestamp { iso unix __typename } __typename }}", + "operationName": "GetBatteryData", + "variables": "{\"vin\":\"" + self.vin + "\"}" + } + return await self.get_graph_ql(params) + + async def get_vehicle_data(self): + # get Vehicle Data + params = { + "query": "query getCars { getConsumerCarsV2 { vin internalVehicleIdentifier modelYear content { model { code name __typename } images { studio { url angles __typename } __typename } __typename } hasPerformancePackage registrationNo deliveryDate currentPlannedDeliveryDate __typename }}", + "operationName": "getCars", + "variables": "{}" + } + return await self.get_graph_ql(params) diff --git a/custom_components/polestar_api/sensor.py b/custom_components/polestar_api/sensor.py index 9721442..711be05 100644 --- a/custom_components/polestar_api/sensor.py +++ b/custom_components/polestar_api/sensor.py @@ -40,11 +40,10 @@ class PolestarSensorDescriptionMixin: """Define an entity description mixin for sensor entities.""" - path: str - unit: str + query: str + field_name: str round_digits: int | None unit: str | None - response_path: str | None max_value: int | None @@ -56,17 +55,18 @@ class PolestarSensorDescription( ChargingConnectionStatusDict = { - "CONNECTION_STATUS_DISCONNECTED": "Disconnected", - "CONNECTION_STATUS_CONNECTED_AC": "Connected AC", - "CONNECTION_STATUS_CONNECTED_DC": "Connected DC", - "CONNECTION_STATUS_UNSPECIFIED": "Unspecified", + "CHARGER_CONNECTION_STATUS_CONNECTED": "Connected", + "CHARGER_CONNECTION_STATUS_DISCONNECTED": "Disconnected", } ChargingSystemStatusDict = { - "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified", - "CHARGING_SYSTEM_CHARGING": "Charging", - "CHARGING_SYSTEM_IDLE": "Idle", - "CHARGING_SYSTEM_FAULT": "Fault", + "CHARGING_STATUS_DONE": "Done", + "CHARGING_STATUS_IDLE": "Idle", + "CHARGING_STATUS_CHARGING": "Charging", + "CHARGING_STATUS_FAULT": "Fault", + "CHARGING_STATUS_UNSPECIFIED": "Unspecified", + "CHARGING_STATUS_SCHEDULED": "Scheduled", + } API_STATUS_DICT = { @@ -78,126 +78,218 @@ class PolestarSensorDescription( POLESTAR_SENSOR_TYPES: Final[tuple[PolestarSensorDescription, ...]] = ( PolestarSensorDescription( - key="estimate_full_charge_range", - name="Est. full charge range", + key="estimate_distance_to_empty_miles", + name="Est. distance miles", icon="mdi:map-marker-distance", - path="{vin}/recharge-status", - response_path=None, - unit='km', + query="getBatteryData", + field_name="estimatedDistanceToEmptyMiles", + unit='miles', round_digits=None, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, max_value=None ), PolestarSensorDescription( - key="estimate_full_charge_range_miles", - name="Est. full charge range", + key="estimate_distance_to_empty_km", + name="Est. distance km", icon="mdi:map-marker-distance", - path="{vin}/recharge-status", - response_path=None, - unit='miles', + query="getBatteryData", + field_name="estimatedDistanceToEmptyKm", + unit='km', round_digits=None, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, max_value=None ), PolestarSensorDescription( - key="battery_charge_level", - name="Battery level", - path="{vin}/recharge-status", - response_path="batteryChargeLevel.value", - unit=PERCENTAGE, - round_digits=0, + key="current_odometer_meters", + name="Odometer Meter", + icon="mdi:map-marker-distance", + query="getOdometerData", + field_name="odometerMeters", + unit='km', + round_digits=None, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.BATTERY, - max_value=None, + device_class=SensorDeviceClass.DISTANCE, + max_value=None ), PolestarSensorDescription( - key="last_updated", - name="Last updated", - path="{vin}/recharge-status", - response_path="batteryChargeLevel.timestamp", - unit=None, + key="average_speed_km_per_hour", + name="Average Speed Per Hour", + icon="mdi:map-marker-distance", + query="getOdometerData", + field_name="averageSpeedKmPerHour", + unit='km', round_digits=None, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.TIMESTAMP, - max_value=None, + device_class=SensorDeviceClass.DISTANCE, + max_value=None ), PolestarSensorDescription( - key="electric_range", - name="EV Range", + key="current_trip_meter_automatic", + name="Trip Meter Automatic", icon="mdi:map-marker-distance", - path="{vin}/recharge-status", - response_path="electricRange.value", + query="getOdometerData", + field_name="tripMeterAutomaticKm", unit='km', round_digits=None, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, - max_value=570, # prevent spike value, and this should be the max range of polestar + max_value=None ), PolestarSensorDescription( - key="electric_range_miles", - name="EV Range", + key="current_trip_meter_manual", + name="Trip Meter Manual", icon="mdi:map-marker-distance", - path="{vin}/recharge-status", - response_path="electricRange.value", - unit='miles', + query="getOdometerData", + field_name="tripMeterManualKm", + unit='km', round_digits=None, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, - max_value=355, # prevent spike value, and this should be the max range of polestar + max_value=None ), PolestarSensorDescription( - key="estimated_charging_time", + key="battery_charge_level", + name="Battery level", + query="getBatteryData", + field_name="batteryChargeLevelPercentage", + unit=PERCENTAGE, + round_digits=0, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + max_value=None, + ), + PolestarSensorDescription( + key="estimated_charging_time_to_full_minutes", name="Charging time", icon="mdi:battery-clock", - path="{vin}/recharge-status", - response_path="estimatedChargingTime.value", + query="getBatteryData", + field_name="estimatedChargingTimeToFullMinutes", unit='Minutes', round_digits=None, max_value=None, ), PolestarSensorDescription( - key="estimated_fully_charged_time", - name="Fully charged time", + key="charging_status", + name="Charging status", icon="mdi:battery-clock", - path="{vin}/recharge-status", - response_path="estimatedChargingTime.value", + query="getBatteryData", + field_name="chargingStatus", unit=None, round_digits=None, + max_value=None, + ), + PolestarSensorDescription( + key="charging_power_watts", + name="Charging Watt", + icon="mdi:lightning-bolt", + query="getBatteryData", + field_name="chargingPowerWatts", + unit='W', + round_digits=None, + max_value=None, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.DURATION, + device_class=SensorDeviceClass.POWER, + ), + PolestarSensorDescription( + key="charging_current_amps", + name="Charging Amps", + icon="mdi:current-ac", + query="getBatteryData", + field_name="chargingCurrentAmps", + unit='A', + round_digits=None, + max_value=None, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + PolestarSensorDescription( + key="charger_connection_status", + name="Charging power", + icon="mdi:battery-clock", + query="getBatteryData", + field_name="chargerConnectionStatus", + unit=None, + round_digits=None, + max_value=None, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + PolestarSensorDescription( + key="average_energy_consumption_kwh_per_100_km", + name="Average energy consumption", + icon="mdi:battery-clock", + query="getBatteryData", + field_name="averageEnergyConsumptionKwhPer100Km", + unit='Kwh/100km', + round_digits=None, + max_value=None, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + PolestarSensorDescription( + key="estimated_charging_time_minutes_to_target_distance", + name="Estimated charging time to target distance", + icon="mdi:battery-clock", + query="getBatteryData", + field_name="estimatedChargingTimeMinutesToTargetDistance", + unit=PERCENTAGE, + round_digits=None, + max_value=None, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + PolestarSensorDescription( + key="vin", + name="VIN", + icon="mdi:card-account-details", + query="getConsumerCarsV2", + field_name="vin", + unit=None, + round_digits=None, + max_value=None, + ), + PolestarSensorDescription( + key="registration_number", + name="Registration number", + icon="mdi:numeric-1-box", + query="getConsumerCarsV2", + field_name="registrationNo", + unit=None, + round_digits=None, max_value=None, ), PolestarSensorDescription( - key="charging_connection_status", - name="Charg. connection status", - icon="mdi:car", - path="{vin}/recharge-status", - response_path="chargingConnectionStatus.value", + key="estimated_fully_charged_time", + name="Est. Full charged", + icon="mdi:battery-clock", + query="getBatteryData", + field_name="estimatedChargingTimeToFullMinutes", unit=None, round_digits=None, state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, max_value=None, ), PolestarSensorDescription( - key="charging_system_status", - name="Charg. system status", - icon="mdi:car", - path="{vin}/recharge-status", - response_path="chargingSystemStatus.value", + key="model_name", + name="Model name", + query="getConsumerCarsV2", + field_name="content/model/name", unit=None, round_digits=None, max_value=None, ), PolestarSensorDescription( - key="api_status_code", - name="API status", - icon="mdi:heart", - path=None, - response_path=None, + key="last_updated", + name="Last updated", + query="getOdometerData", + field_name="eventUpdatedTimestamp/iso", unit=None, round_digits=None, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TIMESTAMP, max_value=None, ), @@ -221,7 +313,7 @@ async def async_setup_entry( device: PolestarApi device = hass.data[POLESTAR_API_DOMAIN][entry.entry_id] # put data in cache - await device.get_data("{vin}/recharge-status") + await device.get_ev_data() sensors = [ PolestarSensor(device, description) for description in POLESTAR_SENSOR_TYPES @@ -260,12 +352,16 @@ def _get_current_value(self) -> StateType | None: """Get the current value.""" return self.async_update() + def get_skip_cache(self) -> bool: + """Get the skip cache.""" + return self.description.key in ('vin', 'registration_number', 'model_name') + @callback def _async_update_attrs(self) -> None: """Update the state and attributes.""" # try to fill the current cache data self._attr_native_value = self._device.get_cache_data( - self.description.path, self.description.response_path) + self.description.query, self.description.field_name, self.get_skip_cache()) @property def unique_id(self) -> str: @@ -303,9 +399,9 @@ def state(self) -> StateType: return None # parse the long text with a shorter one from the dict - if self.entity_description.key == 'charging_connection_status': + if self.entity_description.key == 'charger_connection_status': return ChargingConnectionStatusDict.get(self._attr_native_value, self._attr_native_value) - if self.entity_description.key == 'charging_system_status': + if self.entity_description.key == 'charging_status': return ChargingSystemStatusDict.get(self._attr_native_value, self._attr_native_value) # battery charge level contain ".0" at the end, this should be removed @@ -357,12 +453,26 @@ def state(self) -> StateType: return estimate_range - if self.entity_description.key == 'electric_range_miles': + if self.entity_description.key == 'current_odometer_meters' and int(self._attr_native_value) > 1000: + self._attr_native_value = int(self._attr_native_value) + km = round(self._attr_native_value / 1000, + self.entity_description.round_digits if self.entity_description.round_digits is not None else 0) + + return km + + if self.entity_description.key == 'current_trip_meter_manual' and int(self._attr_native_value) > 1000: + self._attr_native_value = int(self._attr_native_value) + km = round(self._attr_native_value / 1000, + self.entity_description.round_digits if self.entity_description.round_digits is not None else 0) + + return km + + if self.entity_description.key == 'current_trip_meter_automatic' and int(self._attr_native_value) > 1000: self._attr_native_value = int(self._attr_native_value) - miles = round(self._attr_native_value / 1.609344, - self.entity_description.round_digits if self.entity_description.round_digits is not None else 0) + km = round(self._attr_native_value / 1000, + self.entity_description.round_digits if self.entity_description.round_digits is not None else 0) - return miles + return km return self._attr_native_value @property @@ -372,9 +482,12 @@ def unit_of_measurement(self) -> str: async def async_update(self) -> None: """Get the latest data and updates the states.""" - data = await self._device.get_data(self.entity_description.path, self.entity_description.response_path) + data = self._device.get_cache_data( + self.entity_description.query, self.entity_description.field_name, self.get_skip_cache()) if data is None: - return + data = self._device.get_cache_data( + self.entity_description.query, self.entity_description.field_name, True) + await self._device.get_ev_data() self._attr_native_value = data self.value = data diff --git a/custom_components/polestar_api/strings.json b/custom_components/polestar_api/strings.json index 669f8e3..1a2104f 100644 --- a/custom_components/polestar_api/strings.json +++ b/custom_components/polestar_api/strings.json @@ -8,8 +8,7 @@ "name": "Friendly name", "username": "Username", "password": "Password", - "vin": "VIN", - "vcc_api_key": "VCC API Key" + "vin": "VIN" } } }, diff --git a/custom_components/polestar_api/translations/en.json b/custom_components/polestar_api/translations/en.json index 669f8e3..1a2104f 100644 --- a/custom_components/polestar_api/translations/en.json +++ b/custom_components/polestar_api/translations/en.json @@ -8,8 +8,7 @@ "name": "Friendly name", "username": "Username", "password": "Password", - "vin": "VIN", - "vcc_api_key": "VCC API Key" + "vin": "VIN" } } },