Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store access token in entry for Fyta #116260

Merged
merged 12 commits into from
Apr 29, 2024
15 changes: 13 additions & 2 deletions homeassistant/components/fyta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

from datetime import datetime
import logging
from zoneinfo import ZoneInfo

from fyta_cli.fyta_connector import FytaConnector

Expand All @@ -22,11 +24,20 @@

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Fyta integration."""
tz: str = hass.config.time_zone

username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]

fyta = FytaConnector(username, password)
access_token: str = entry.data.get("access_token", "")
expiration: datetime | None = (
datetime.fromisoformat(entry.data.get("expiration", "")).astimezone(
ZoneInfo(tz)
)
if "expiration" in entry.data
else None
)

fyta = FytaConnector(username, password, access_token, expiration, tz)

coordinator = FytaCoordinator(hass, fyta)

Expand Down
9 changes: 8 additions & 1 deletion homeassistant/components/fyta/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import Mapping
from datetime import datetime
import logging
from typing import Any

Expand Down Expand Up @@ -32,13 +33,14 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):

VERSION = 1
_entry: ConfigEntry | None = None
credentials: dict[str, str | datetime] = {}
dontinelli marked this conversation as resolved.
Show resolved Hide resolved

async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
"""Reusable Auth Helper."""
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])

try:
await fyta.login()
self.credentials = await fyta.login()
except FytaConnectionError:
return {"base": "cannot_connect"}
except FytaAuthentificationError:
Expand All @@ -51,6 +53,9 @@ async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
finally:
await fyta.client.close()

if isinstance(self.credentials["expiration"], datetime):
self.credentials["expiration"] = self.credentials["expiration"].isoformat()

return {}

async def async_step_user(
Expand All @@ -62,6 +67,7 @@ async def async_step_user(
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})

if not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
Expand All @@ -85,6 +91,7 @@ async def async_step_reauth_confirm(
assert self._entry is not None

if user_input and not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
return self.async_update_reload_and_abort(
self._entry, data={**self._entry.data, **user_input}
)
Expand Down
25 changes: 22 additions & 3 deletions homeassistant/components/fyta/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,36 @@ async def _async_update_data(
) -> dict[int, dict[str, Any]]:
"""Fetch data from API endpoint."""

if self.fyta.expiration is None or self.fyta.expiration < datetime.now():
if (
self.fyta.expiration is None
or self.fyta.expiration.timestamp() < datetime.now().timestamp()
):
await self.renew_authentication()

return await self.fyta.update_all_plants()

async def renew_authentication(self) -> None:
async def renew_authentication(self) -> bool:
"""Renew access token for FYTA API."""
credentials: dict[str, str | datetime] = {}

try:
await self.fyta.login()
credentials = await self.fyta.login()
except FytaConnectionError as ex:
raise ConfigEntryNotReady from ex
except (FytaAuthentificationError, FytaPasswordError) as ex:
raise ConfigEntryAuthFailed from ex

if isinstance(credentials["expiration"], datetime):
credentials["expiration"] = credentials["expiration"].isoformat()

new_config_entry = {**self.config_entry.data}
new_config_entry["access_token"] = credentials.get("access_token")
new_config_entry["expiration"] = credentials.get("expiration")
dontinelli marked this conversation as resolved.
Show resolved Hide resolved

self.hass.config_entries.async_update_entry(
self.config_entry, data=new_config_entry
)

_LOGGER.info("Credentials successfully updated")
dontinelli marked this conversation as resolved.
Show resolved Hide resolved

return True
7 changes: 6 additions & 1 deletion tests/components/fyta/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import pytest

from .test_config_flow import ACCESS_TOKEN, EXPIRATION


@pytest.fixture
def mock_fyta():
Expand All @@ -15,7 +17,10 @@ def mock_fyta():
"homeassistant.components.fyta.config_flow.FytaConnector",
return_value=mock_fyta_api,
) as mock_fyta_api:
mock_fyta_api.return_value.login.return_value = {}
mock_fyta_api.return_value.login.return_value = {
"access_token": ACCESS_TOKEN,
"expiration": EXPIRATION,
}
yield mock_fyta_api


Expand Down
21 changes: 19 additions & 2 deletions tests/components/fyta/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test the fyta config flow."""
dontinelli marked this conversation as resolved.
Show resolved Hide resolved

from datetime import UTC, datetime
from unittest.mock import AsyncMock

from fyta_cli.fyta_exceptions import (
Expand All @@ -19,6 +20,8 @@

USERNAME = "fyta_user"
PASSWORD = "fyta_pass"
ACCESS_TOKEN = "123xyz"
EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC)


async def test_user_flow(
Expand All @@ -39,7 +42,12 @@ async def test_user_flow(

assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == USERNAME
assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
assert result2["data"] == {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"access_token": ACCESS_TOKEN,
"expiration": "2024-12-31T10:00:00+00:00",
}
assert len(mock_setup_entry.mock_calls) == 1


Expand Down Expand Up @@ -89,6 +97,8 @@ async def test_form_exceptions(
assert result["title"] == USERNAME
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert result["data"]["access_token"] == ACCESS_TOKEN
assert result["data"]["expiration"] == "2024-12-31T10:00:00+00:00"

assert len(mock_setup_entry.mock_calls) == 1

Expand Down Expand Up @@ -141,7 +151,12 @@ async def test_reauth(
entry = MockConfigEntry(
domain=DOMAIN,
title=USERNAME,
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
data={
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"access_token": "old_token",
"expiration": "2024-06-30T10:00:00+00:00",
},
)
entry.add_to_hass(hass)

Expand Down Expand Up @@ -178,5 +193,7 @@ async def test_reauth(
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_USERNAME] == "other_username"
assert entry.data[CONF_PASSWORD] == "other_password"
assert entry.data["access_token"] == ACCESS_TOKEN
assert entry.data["expiration"] == "2024-12-31T10:00:00+00:00"

assert len(mock_setup_entry.mock_calls) == 1