Skip to content

Commit

Permalink
feat: introduce access_token retention
Browse files Browse the repository at this point in the history
feat: introduce reauthentication flow

fix: removed client full cookie clearing (absolutely redundant)

refactor: configuration entry unique id binds to user id, not username

chore: bump version number (v2023.6.3 -> v2023.6.4)
  • Loading branch information
alryaz committed Jun 26, 2023
1 parent 98f20fc commit 06302ff
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 91 deletions.
49 changes: 30 additions & 19 deletions custom_components/pandora_cas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
CONF_USERNAME,
Platform,
CONF_VERIFY_SSL,
CONF_ACCESS_TOKEN,
)
from homeassistant.core import ServiceCall, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, ConfigEntryAuthFailed
Expand All @@ -49,6 +50,9 @@
TrackingPoint,
Features,
)
from custom_components.pandora_cas.config_flow import (
async_authenticate_account,
)
from custom_components.pandora_cas.entity import (
PandoraCASEntity,
PandoraCASUpdateCoordinator,
Expand Down Expand Up @@ -262,30 +266,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

# create account object
account = PandoraOnlineAccount(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
access_token=entry.data.get(CONF_ACCESS_TOKEN),
session=async_get_clientsession(
hass, verify_ssl=entry.options[CONF_VERIFY_SSL]
),
)

# Attempt authentication
try:
_LOGGER.debug(f'Authenticating entry "{entry.entry_id}"')
await account.async_authenticate()

except PandoraOnlineException as error:
_LOGGER.error(f"Error authenticating: {error}", exc_info=error)
raise ConfigEntryAuthFailed(str(error)) from error

# Attempt devices fetching
try:
_LOGGER.debug(f'Fetching devices for account "{entry.entry_id}"')
await account.async_update_vehicles()
await async_authenticate_account(account)

except PandoraOnlineException as error:
_LOGGER.error(f"Error updating vehicles: {error}", exc_info=error)
raise ConfigEntryNotReady(str(error)) from error
if entry.data.get(CONF_ACCESS_TOKEN) != account.access_token:
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_ACCESS_TOKEN: account.access_token}
)

hass.data.setdefault(DOMAIN, {})[
entry.entry_id
Expand Down Expand Up @@ -430,9 +424,26 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if entry.version < 4:
new_options.setdefault(CONF_VERIFY_SSL, True)
new_options.setdefault(CONF_DISABLE_WEBSOCKETS, False)
new_unique_id = entry.unique_id or entry.data[CONF_USERNAME]
# new_unique_id = entry.unique_id or entry.data[CONF_USERNAME]
entry.version = 4

if entry.version < 5:
account = PandoraOnlineAccount(
username=new_data[CONF_USERNAME],
password=new_data[CONF_PASSWORD],
access_token=new_data.get(CONF_ACCESS_TOKEN),
session=async_get_clientsession(
hass, verify_ssl=new_options[CONF_VERIFY_SSL]
),
)

await async_authenticate_account(account, no_device_update=True)

new_data[CONF_ACCESS_TOKEN] = account.access_token
new_unique_id = str(account.user_id)

entry.version = 5

hass.config_entries.async_update_entry(
entry,
data=new_data,
Expand Down
58 changes: 34 additions & 24 deletions custom_components/pandora_cas/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,16 +551,13 @@ def __init__(

self._username = username
self._password = password
self._access_token = access_token
self.access_token = access_token
self._user_id: Optional[int] = None
self._session = session

#: last update timestamp
self._last_update = -1

#: properties generated upon authentication
self._session_id = None
self._user_id = None

#: list of vehicles associated with this account.
self._devices: List[PandoraOnlineDevice] = list()

Expand All @@ -575,16 +572,25 @@ async def async_close(self):

def __repr__(self):
"""Retrieve representation of account object"""
return "<" + str(self) + ">"
return f"<{self}>"

def __str__(self):
return '%s[username="%s"]' % (self.__class__.__name__, self._username)
return (
f"{self.__class__.__name__}["
f'username="{self.username}", '
f"user_id={self.user_id}"
f"]"
)

# Basic properties
@property
def utc_offset(self) -> int:
return self._utc_offset

@property
def user_id(self) -> Optional[int]:
return self._user_id

@property
def username(self) -> str:
"""Username accessor."""
Expand Down Expand Up @@ -671,14 +677,12 @@ async def async_fetch_access_token(self) -> str:
async def async_authenticate(self, access_token: Optional[str] = None):
_LOGGER.debug('Authenticating with username "%s"' % (self._username,))

self._session.cookie_jar.clear()

if access_token is None:
access_token = await self.async_fetch_access_token()

url = self.BASE_URL + "/api/users/login"
request_data = {
"login": self._username,
"login": (username := self._username),
"password": self._password,
"lang": "ru",
"v": "3",
Expand All @@ -688,19 +692,19 @@ async def async_authenticate(self, access_token: Optional[str] = None):

async with self._session.post(url, data=request_data) as response:
try:
await self._handle_response(response)
resp = await self._handle_response(response)
except RequestException as e:
raise AuthenticationException(*e.args) from None
self._user_id = user_id = int(resp["user_id"])
self.access_token = access_token

_LOGGER.info(
'Authentication successful for username "%s"!'
% (self._username,)
f'Authentication successful for username "{username}" (user ID: {user_id})!'
)
self._access_token = access_token

async def async_update_vehicles(self):
"""Retrieve and cache list of vehicles for the account."""
access_token = self._access_token
access_token = self.access_token
if access_token is None:
raise PandoraOnlineException("Account is not authenticated")

Expand All @@ -710,7 +714,7 @@ async def async_update_vehicles(self):

async with self._session.get(
self.BASE_URL + "/api/devices",
params={"access_token": self._access_token},
params={"access_token": self.access_token},
) as response:
devices_data = await self._handle_response(response)
_LOGGER.debug("retrieved devices: %s", devices_data)
Expand All @@ -734,7 +738,7 @@ async def async_update_vehicles(self):
async def async_remote_command(
self, device_id: int, command_id: Union[int, "CommandID"]
):
access_token = self._access_token
access_token = self.access_token
if access_token is None:
raise PandoraOnlineException("Account is not authenticated")

Expand All @@ -745,7 +749,7 @@ async def async_remote_command(
async with self._session.post(
self.BASE_URL + "/api/devices/command",
data={"id": device_id, "command": int(command_id)},
params={"access_token": self._access_token},
params={"access_token": self.access_token},
) as response:
command_result = await self._handle_response(response)
status = command_result.get("action_result", {}).get(
Expand All @@ -766,13 +770,15 @@ async def async_request_updates(
) -> Dict[int, Dict[str, Any]]:
"""
Fetch the latest changes from update server.
:param timestamp:
:return: (New data, Set of updated device IDs)
:param timestamp: Timestamp to fetch updates since (optional, uses
last update timestamp internally if not provided).
:return: Dictionary of (device_id => (state_attribute => new_value))
"""
access_token = self._access_token
access_token = self.access_token
if access_token is None:
raise PandoraOnlineException("Account is not authenticated")

# Select last timestamp if none provided
_timestamp = self._last_update if timestamp is None else timestamp

_LOGGER.debug(f"Fetching changes since {_timestamp} on account {self}")
Expand All @@ -793,15 +799,19 @@ async def async_request_updates(
("stats", self._process_http_stats),
("time", self._process_http_times),
):
# Check if response contains necessary data
if not (mapping := content.get(key)):
continue

# Iterate over device responses
for device_id, data in mapping.items():
if device := self.get_device(device_id):
# Process attributes and merge into final dict
_, new_attrs = meth(device, data)
try:
device_new_attrs[int(device_id)].update(new_attrs)
device_new_attrs[device.device_id].update(new_attrs)
except KeyError:
device_new_attrs[int(device_id)] = new_attrs
device_new_attrs[device.device_id] = new_attrs
else:
_LOGGER.debug(
f'Device with ID "{device_id}" {key} data '
Expand Down Expand Up @@ -1287,7 +1297,7 @@ async def async_listen_for_updates(
try:
async with self._session.ws_connect(
self.BASE_URL
+ f"/api/v4/updates/ws?access_token={self._access_token}"
+ f"/api/v4/updates/ws?access_token={self.access_token}"
) as ws:
_LOGGER.debug(f"[{self}] WebSockets connected")

Expand Down
Loading

0 comments on commit 06302ff

Please sign in to comment.