diff --git a/docs/usage.md b/docs/usage.md index 34f5436..493c399 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -76,7 +76,7 @@ In order to actually control the unit you will need to access the cloud in order ```python cloud_api = IntelliFireAPICloud(use_http=True, verify_ssl=False) -await cloud_api.login(username=username, password=password) +await cloud_api.login_with_credentials(username=username, password=password) # Once logged in you can pull out the api key for the default (first detected) fireplace api_key = cloud_api.get_fireplace_api_key(cloud_api.default_fireplace) diff --git a/example_cloud_info.py b/example_cloud_info.py index ae04da2..56b9e0e 100644 --- a/example_cloud_info.py +++ b/example_cloud_info.py @@ -4,12 +4,11 @@ import logging import os +from httpx import Cookies from rich import print from rich.logging import RichHandler -from intellifire4py import UnifiedFireplace from intellifire4py.cloud_interface import IntelliFireCloudInterface -from intellifire4py.const import IntelliFireApiMode FORMAT = "%(message)s" logging.basicConfig( @@ -54,25 +53,26 @@ async def main() -> None: cloud_api_interface = IntelliFireCloudInterface(use_http=True, verify_ssl=False) - user_json: str = os.getenv("USER_JSON") # type: ignore - - cloud_api_interface.load_user_data(json_str=user_json) - - # await cloud_api_interface.login(username=username, password=password) - - user_data = cloud_api_interface.user_data - print(user_data.model_dump_json(indent=2)) - - fireplaces: UnifiedFireplace = ( - await UnifiedFireplace.build_fireplaces_from_user_data(user_data) - ) - fireplace = fireplaces[0] - - await fireplace.set_read_mode(IntelliFireApiMode.LOCAL) - await fireplace.set_control_mode(IntelliFireApiMode.CLOUD) - - print(fireplace.user_data_json) - exit(0) + await cloud_api_interface.login_with_cookie(cookie=Cookies()) + # user_json: str = os.getenv("USER_JSON") # type: ignore + # + # cloud_api_interface.load_user_data(json_str=user_json) + # + # # await cloud_api_interface.login(username=username, password=password) + # + # user_data = cloud_api_interface.user_data + # print(user_data.model_dump_json(indent=2)) + # + # fireplaces: UnifiedFireplace = ( + # await UnifiedFireplace.build_fireplaces_from_user_data(user_data) + # ) + # fireplace = fireplaces[0] + # + # await fireplace.set_read_mode(IntelliFireApiMode.LOCAL) + # await fireplace.set_control_mode(IntelliFireApiMode.CLOUD) + # + # print(fireplace.user_data_json) + # exit(0) # fireplace[0].debug() # await asyncio.sleep(10) diff --git a/src/intellifire4py/cloud_interface.py b/src/intellifire4py/cloud_interface.py index e22b253..4fa58ff 100644 --- a/src/intellifire4py/cloud_interface.py +++ b/src/intellifire4py/cloud_interface.py @@ -51,7 +51,57 @@ def __init__(self, use_http: bool = False, verify_ssl: bool = True): else: self.prefix = "https" - async def login(self, *, username: str, password: str) -> None: + async def login_with_cookie_vars( + self, *, user_id: str, auth_cookie: str, web_client_id: str + ) -> None: + """Logs in using individual cookie components instead of a pre-formed cookie object. + + This method constructs a cookie using the provided user_id, auth_cookie, and web_client_id, + then proceeds with the login process using this cookie. It's an alternative way to authenticate + when you have the cookie components instead of a full cookie string. + + Args: + user_id (str): The user ID part of the cookie. + auth_cookie (str): The authentication token part of the cookie. + web_client_id (str): The web client ID part of the cookie. + + Returns: + None: This method does not return anything. It sets the authenticated state internally. + """ + cookie = Cookies() + cookie.set("user", user_id) + cookie.set("auth_cookie", auth_cookie) + cookie.set("web_client_id", web_client_id) + return await self.login_with_cookie(cookie=cookie) + + async def login_with_cookie(self, *, cookie: Cookies) -> None: + """Uses a cookie 🍪️ to simulate the login flow, bypassing the need for username and password. + + Sets the user as logged in if the cookie is valid and can successfully fetch user data. + """ + self._user_data.username = "UNSET" + self._user_data.password = "UNSET" # noqa: S105 + + self._cookie = cookie + self._user_data.parse_cookie(self._cookie) + + # Must be set for future methods to pass -> if cookie is invalid will be set to false + self._is_logged_in = True + + async with httpx.AsyncClient() as client: + try: + self._log.info("Using cookie data to poll IFTAPI") + await self._parse_user_data(client=client) + except httpx.HTTPError as http_err: + self._log.error(f"HTTP error occurred: {http_err}") + self._is_logged_in = False + # raise + except Exception as err: + self._log.error(f"An error occurred: {err}") + self._is_logged_in = False + raise + + async def login_with_credentials(self, *, username: str, password: str) -> None: """Authenticates with the IntelliFire Cloud API using the provided username and password. This method performs a login operation to the cloud API, storing the session cookies @@ -174,44 +224,58 @@ async def _parse_user_data(self, client: httpx.AsyncClient) -> None: async def _get_locations(self, client: httpx.AsyncClient) -> IntelliFireLocations: """Retrieves a list of locations accessible to the user from the cloud API. - This method makes an API call to gather details about locations that the user has access to. - The retrieved locations are used to discover fireplaces and their respective data. - Args: client (httpx.AsyncClient): The HTTP client used for making the request. Returns: IntelliFireLocations: An object representing the locations accessible to the user. + + Raises: + httpx.HTTPError: If there's an HTTP error during the request. """ await self._login_check() - response = await client.get(url=f"{self.prefix}://iftapi.net/a/enumlocations") - json_data = response.json() - locations = IntelliFireLocations(**json_data) - return locations + + try: + response = await client.get( + url=f"{self.prefix}://iftapi.net/a/enumlocations" + ) + response.raise_for_status() # Raises an HTTPError for 4xx/5xx responses + json_data = response.json() + locations = IntelliFireLocations(**json_data) + return locations + except httpx.HTTPError as e: + self._log.error(f"HTTP error occurred while fetching locations: {e}") + raise async def _get_fireplaces( self, client: httpx.AsyncClient, *, location_id: str ) -> IntelliFireFireplaces: """Retrieves a list of fireplaces associated with a given location. - This method queries the cloud API to obtain detailed information about fireplaces present - at a specific location identified by the location_id. - Args: client (httpx.AsyncClient): The HTTP client used for making the request. location_id (str): Identifier for the location whose fireplaces are to be retrieved. Returns: IntelliFireFireplaces: An object containing details of fireplaces at the specified location. + + Raises: + httpx.HTTPError: If there's an HTTP error during the request. """ await self._login_check() - response = await client.get( - url=f"{self.prefix}://iftapi.net/a/enumfireplaces?location_id={location_id}" - ) - json_data = response.json() - fireplaces = IntelliFireFireplaces(**json_data) - return fireplaces + try: + response = await client.get( + url=f"{self.prefix}://iftapi.net/a/enumfireplaces?location_id={location_id}" + ) + response.raise_for_status() # Raises an HTTPError for 4xx/5xx responses + json_data = response.json() + + fireplaces = IntelliFireFireplaces(**json_data) + return fireplaces + except httpx.HTTPError as e: + self._log.error(f"HTTP error occurred while fetching fireplaces: {e}") + raise @property def cloud_fireplaces(self) -> list[IntelliFireAPICloud]: diff --git a/src/intellifire4py/unified_fireplace.py b/src/intellifire4py/unified_fireplace.py index 2bdc728..5a557ee 100644 --- a/src/intellifire4py/unified_fireplace.py +++ b/src/intellifire4py/unified_fireplace.py @@ -78,6 +78,10 @@ def __init__( serial=self.serial, cookies=self._fireplace_data.cookies ) + async def perform_cloud_poll(self): + """Perform a Cloud Poll - this should be used to validate the stored credentials.""" + await self._cloud_api.poll() + @property def dump_user_data_json(self) -> str: """Dump the internal _fireplace_data object to a JSON String.""" diff --git a/tests/test_cloud_api.py b/tests/test_cloud_api.py index a431e45..dccceb5 100644 --- a/tests/test_cloud_api.py +++ b/tests/test_cloud_api.py @@ -51,7 +51,7 @@ async def test_cloud_login( cloud_interface = IntelliFireCloudInterface() # cloud_api = IntelliFireAPICloud(serial="XXXXXE834CE109D849CBB15CDDBAFF381") - await cloud_interface.login(username=username, password=password) + await cloud_interface.login_with_credentials(username=username, password=password) user_data = cloud_interface.user_data fireplaces = await UnifiedFireplace.build_fireplaces_from_user_data( user_data, @@ -105,7 +105,7 @@ async def test_incorrect_login_credentials(httpx_mock: HTTPXMock) -> None: cloud_api = IntelliFireCloudInterface() with pytest.raises(LoginError): - await cloud_api.login(username=username, password=password) + await cloud_api.login_with_credentials(username=username, password=password) @pytest.mark.asyncio @@ -157,7 +157,7 @@ async def test_sending( ) cloud_api = IntelliFireCloudInterface() - await cloud_api.login(username=username, password=password) + await cloud_api.login_with_credentials(username=username, password=password) # Fake logged in state # cloud_api = IntelliFireAPICloud()