Skip to content

Commit

Permalink
remove vin in config_flow, add more logging, add api status
Browse files Browse the repository at this point in the history
  • Loading branch information
Tuen Lee committed Dec 20, 2023
1 parent 99a65b8 commit 276f8d0
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 37 deletions.
3 changes: 1 addition & 2 deletions custom_components/polestar_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@


from .const import (
CONF_VIN,
DOMAIN,
TIMEOUT
)
Expand All @@ -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, {})
Expand Down
25 changes: 11 additions & 14 deletions custom_components/polestar_api/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

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

Expand All @@ -57,24 +55,23 @@ 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."""
if user_input is 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])
2 changes: 0 additions & 2 deletions custom_components/polestar_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
TIMEOUT = 90


CONF_VIN = "vin"

ACCESS_TOKEN_MANAGER_ID = "JWTh4Yf0b"
GRANT_TYPE = "password"
AUTHORIZATION = "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc="
Expand Down
3 changes: 2 additions & 1 deletion custom_components/polestar_api/entity.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
59 changes: 44 additions & 15 deletions custom_components/polestar_api/polestar_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,29 +23,42 @@ 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
disable_warnings()

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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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']
Expand Down Expand Up @@ -204,15 +230,18 @@ 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
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'))
else:
# log the error
_LOGGER.warning(resultData.get('errors'))
self.latest_call_code = 500 # set internal error
_LOGGER.debug(resultData)
return resultData

Expand Down
19 changes: 16 additions & 3 deletions custom_components/polestar_api/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -72,7 +72,8 @@ class PolestarSensorDescription(
API_STATUS_DICT = {
200: "OK",
401: "Unauthorized",
404: "API Down"
404: "API Down",
500: "Internal Server Error",
}


Expand Down Expand Up @@ -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,
),

)

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

Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit 276f8d0

Please sign in to comment.