From 276f8d0d4c2ccfaf116c0fb861f8fba9d0bcb115 Mon Sep 17 00:00:00 2001 From: Tuen Lee Date: Wed, 20 Dec 2023 16:39:38 +0100 Subject: [PATCH] remove vin in config_flow, add more logging, add api status --- custom_components/polestar_api/__init__.py | 3 +- custom_components/polestar_api/config_flow.py | 25 ++++---- custom_components/polestar_api/const.py | 2 - custom_components/polestar_api/entity.py | 3 +- .../polestar_api/polestar_api.py | 59 ++++++++++++++----- custom_components/polestar_api/sensor.py | 19 +++++- 6 files changed, 74 insertions(+), 37 deletions(-) diff --git a/custom_components/polestar_api/__init__.py b/custom_components/polestar_api/__init__.py index 66154ea..09cca9c 100644 --- a/custom_components/polestar_api/__init__.py +++ b/custom_components/polestar_api/__init__.py @@ -20,7 +20,6 @@ from .const import ( - CONF_VIN, DOMAIN, TIMEOUT ) @@ -43,7 +42,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]) + hass, conf[CONF_USERNAME], conf[CONF_PASSWORD]) 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 a4e0c68..3041ea3 100644 --- a/custom_components/polestar_api/config_flow.py +++ b/custom_components/polestar_api/config_flow.py @@ -5,13 +5,13 @@ from aiohttp import ClientError from async_timeout import timeout import voluptuous as vol +from .polestar_api import PolestarApi from homeassistant import config_entries -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from .polestar import PolestarApi -from .const import CONF_VIN, DOMAIN, TIMEOUT +from .const import DOMAIN, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -23,26 +23,24 @@ 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) -> None: + async def _create_entry(self, username: str, password: str) -> None: """Register new entry.""" return self.async_create_entry( title='Polestar EV', data={ CONF_USERNAME: username, CONF_PASSWORD: password, - CONF_VIN: vin, } ) - async def _create_device(self, username: str, password: str, vin: str) -> None: + async def _create_device(self, username: str, password: str) -> None: """Create device.""" try: device = PolestarApi( self.hass, username, - password, - vin) + password) with timeout(TIMEOUT): await device.init() @@ -57,11 +55,11 @@ async def _create_device(self, username: str, password: str, vin: str) -> None: except ClientError: _LOGGER.exception("ClientError") return self.async_abort(reason="api_failed") - except Exception: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") return self.async_abort(reason="api_failed") - return await self._create_entry(username, password, vin) + return await self._create_entry(username, password, ) async def async_step_user(self, user_input: dict = None) -> None: """User initiated config flow.""" @@ -69,12 +67,11 @@ async def async_step_user(self, user_input: dict = None) -> None: return self.async_show_form( 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_PASSWORD): str }) ) - return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_VIN]) + return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) 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]) + return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) diff --git a/custom_components/polestar_api/const.py b/custom_components/polestar_api/const.py index 42d0bc7..f0898c0 100644 --- a/custom_components/polestar_api/const.py +++ b/custom_components/polestar_api/const.py @@ -2,8 +2,6 @@ TIMEOUT = 90 -CONF_VIN = "vin" - ACCESS_TOKEN_MANAGER_ID = "JWTh4Yf0b" GRANT_TYPE = "password" AUTHORIZATION = "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=" diff --git a/custom_components/polestar_api/entity.py b/custom_components/polestar_api/entity.py index 694775a..ced0abf 100644 --- a/custom_components/polestar_api/entity.py +++ b/custom_components/polestar_api/entity.py @@ -1,6 +1,7 @@ import logging -from .polestar import PolestarApi +from .polestar_api import PolestarApi + from .const import DOMAIN as POLESTAR_API_DOMAIN from homeassistant.helpers.entity import DeviceInfo, Entity diff --git a/custom_components/polestar_api/polestar_api.py b/custom_components/polestar_api/polestar_api.py index da87576..c80a2ca 100644 --- a/custom_components/polestar_api/polestar_api.py +++ b/custom_components/polestar_api/polestar_api.py @@ -5,11 +5,7 @@ from urllib.parse import parse_qs, urlparse from .const import ( - ACCESS_TOKEN_MANAGER_ID, - AUTHORIZATION, CACHE_TIME, - GRANT_TYPE, - HEADER_AUTHORIZATION, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -27,18 +23,17 @@ class PolestarApi: def __init__(self, hass: HomeAssistant, username: str, - password: str, - vin: str, + password: str ) -> None: - self.id = vin[:8] - self.name = "Polestar " + vin[-4:] + self.id = None + self.name = "Polestar " self._session = async_get_clientsession(hass, verify_ssl=False) self.username = username self.password = password self.access_token = None self.token_type = None self.refresh_token = None - self.vin = vin + self.vin = None self.cache_data = {} self.latest_call_code = None self.updating = False @@ -46,10 +41,24 @@ def __init__(self, async def init(self): await self.get_token() + if self.access_token is None: + return result = await self.get_vehicle_data() + + # check if there are cars in the account + if result['data']['getConsumerCarsV2'] is None or len(result['data']['getConsumerCarsV2']) == 0: + _LOGGER.exception("No cars found in account") + # throw new exception + raise Exception("No cars found in account") + self.cache_data['getConsumerCarsV2'] = { 'data': result['data']['getConsumerCarsV2'][0], 'timestamp': datetime.now()} + # fill the vin and id in the constructor + self.vin = result['data']['getConsumerCarsV2'][0]['vin'] + self.id = self.vin[:8] + self.name = "Polestar " + self.vin[-4:] + async def _get_resume_path(self): # Get Resume Path params = { @@ -91,6 +100,7 @@ async def _get_code(self) -> None: params=params, data=data ) + self.latest_call_code = result.status if result.status != 200: _LOGGER.error(f"Error getting code {result.status}") return @@ -101,18 +111,21 @@ async def _get_code(self) -> None: query_params = parse_qs(parsed_url.query) if not query_params.get('code'): - _LOGGER.error(f"Error getting code {result.status}") + _LOGGER.error(f"Error getting code in {query_params}") + _LOGGER.warning("Check if username and password are correct") return code = query_params.get(('code'))[0] # sign-in-callback result = await self._session.get("https://www.polestar.com/sign-in-callback?code=" + code) + self.latest_call_code = result.status if result.status != 200: _LOGGER.error(f"Error getting code callback {result.status}") return # url encode the code result = await self._session.get(url) + self.latest_call_code = result.status return code @@ -132,8 +145,9 @@ async def get_token(self) -> None: "Content-Type": "application/json" } result = await self._session.get("https://pc-api.polestar.com/eu-north-1/auth/", params=params, headers=headers) + self.latest_call_code = result.status if result.status != 200: - _LOGGER.error(f"Error getting code {result.status}") + _LOGGER.error(f"Error getting token {result.status}") return resultData = await result.json() _LOGGER.debug(resultData) @@ -152,18 +166,30 @@ def get_latest_data(self, query: str, field_name: str) -> dict or bool or None: return self._get_field_name_value(field_name, data) def _get_field_name_value(self, field_name: str, data: dict) -> str or bool or None: + if field_name is None: + return None + + if data is None: + return 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] + if data.get(key): + data = data[key] + else: + return None return data 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 query is None: + return None + + if self.cache_data and self.cache_data.get(query): if skip_cache is False: if self.cache_data[query]['timestamp'] + timedelta(seconds=CACHE_TIME) > datetime.now(): data = self.cache_data[query]['data'] @@ -204,6 +230,7 @@ async def get_graph_ql(self, params: dict): } result = await self._session.get("https://pc-api.polestar.com/eu-north-1/my-star/", params=params, headers=headers) + self.latest_call_code = result.status resultData = await result.json() # if auth error, get new token @@ -211,8 +238,10 @@ async def get_graph_ql(self, params: dict): 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')) + else: + # log the error + _LOGGER.warning(resultData.get('errors')) + self.latest_call_code = 500 # set internal error _LOGGER.debug(resultData) return resultData diff --git a/custom_components/polestar_api/sensor.py b/custom_components/polestar_api/sensor.py index c9b2021..ed862a4 100644 --- a/custom_components/polestar_api/sensor.py +++ b/custom_components/polestar_api/sensor.py @@ -25,7 +25,7 @@ from . import DOMAIN as POLESTAR_API_DOMAIN -from .polestar import PolestarApi +from .polestar_api import PolestarApi from homeassistant.const import ( PERCENTAGE, @@ -72,7 +72,8 @@ class PolestarSensorDescription( API_STATUS_DICT = { 200: "OK", 401: "Unauthorized", - 404: "API Down" + 404: "API Down", + 500: "Internal Server Error", } @@ -316,6 +317,16 @@ class PolestarSensorDescription( device_class=SensorDeviceClass.DISTANCE, max_value=None ), + PolestarSensorDescription( + key="api_status_code", + name="API status", + icon="mdi:heart", + query=None, + field_name=None, + unit=None, + round_digits=None, + max_value=None, + ), ) @@ -336,6 +347,7 @@ async def async_setup_entry( # get the device device: PolestarApi device = hass.data[POLESTAR_API_DOMAIN][entry.entry_id] + # put data in cache await device.get_ev_data() @@ -370,7 +382,8 @@ def __init__(self, self._attr_state_class = description.state_class if description.device_class is not None: self._attr_device_class = description.device_class - self._async_update_attrs() + if self._device is not None and self._device.latest_call_code == 200: + self._async_update_attrs() def _get_current_value(self) -> StateType | None: """Get the current value."""