class Client:
    """Define the client."""

    def __init__(
        self,
        *,
        session: Optional[ClientSession] = None,
        request_timeout: int = DEFAULT_TIMEOUT,
    ) -> None:
        """Initialize."""
        self._request_timeout = request_timeout
        self._session = session
        self.controllers: Dict[str, Controller] = {}

    async def _request(
        self,
        method: str,
        url: str,
        *,
        access_token: Optional[str] = None,
        access_token_expiration: Optional[datetime] = None,
        ssl: bool = True,
        **kwargs: Dict[str, Any],
    ) -> dict:
        """Make a request against the RainMachine device."""
        if access_token_expiration and datetime.now() >= access_token_expiration:
            raise TokenExpiredError("Long-lived access token has expired")

        kwargs.setdefault("headers", {})
        kwargs["headers"]["Content-Type"] = "application/json"

        kwargs.setdefault("params", {})
        if access_token:
            kwargs["params"]["access_token"] = access_token

        use_running_session = self._session and not self._session.closed

        if use_running_session:
            session = self._session
        else:
            session = ClientSession(timeout=ClientTimeout(total=DEFAULT_TIMEOUT))

        assert session

        try:
            # Only try 2x for ServerDisconnectedError to comply with the RFC
            # https://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4
            for attempt in range(2):
                try:
                    return await self._request_with_session(
                        session, method, url, ssl, **kwargs
                    )
                except ServerDisconnectedError as err:
                    # The HTTP/1.1 spec allows the device to close the connection
                    # at any time. aiohttp raises ServerDisconnectedError to let us
                    # decide what to do. In this case we want to retry as it likely
                    # means the connection was stale and the server closed it on us.
                    if attempt == 0:
                        continue
                    raise RequestError(
                        f"Error requesting data from {url} ({err}))"
                    ) from err

        finally:
            if not use_running_session:
                await session.close()

        assert False, "unreachable"  # https://github.com/python/mypy/issues/8964

    async def _request_with_session(
        self,
        session: ClientSession,
        method: str,
        url: str,
        ssl: bool,
        **kwargs: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Make a request with a session."""
        try:
            async with async_timeout.timeout(self._request_timeout), session.request(
                method, url, ssl=ssl, **kwargs
            ) as resp:
                resp.raise_for_status()
                data = await resp.json(content_type=None)
                _raise_for_remote_status(url, data)
        except ServerDisconnectedError:
            raise
        except ClientError as err:
            if "401" in str(err):
                raise TokenExpiredError("Long-lived access token has expired") from err
            raise RequestError(f"Error requesting data from {url}") from err
        except asyncio.TimeoutError as err:
            raise RequestError(f"Error requesting data from {url} ({err}))") from err

        _LOGGER.debug("Data received for %s: %s", url, data)
        return cast(Dict[str, Any], data)

    async def load_local(  # pylint: disable=too-many-arguments
        self,
        host: str,
        password: str,
        port: int = DEFAULT_LOCAL_PORT,
        ssl: bool = True,
        skip_existing: bool = True,
    ) -> None:
        """Create a local client."""
        controller: LocalController = LocalController(self._request, host, port, ssl)
        await controller.login(password)

        wifi_data = await controller.provisioning.wifi()
        if skip_existing and wifi_data["macAddress"] in self.controllers:
            return

        version_data = await controller.api.versions()
        controller.api_version = version_data["apiVer"]
        controller.hardware_version = version_data["hwVer"]
        controller.mac = wifi_data["macAddress"]
        controller.name = await controller.provisioning.device_name
        controller.software_version = version_data["swVer"]

        self.controllers[controller.mac] = controller

    async def load_remote(
        self, email: str, password: str, skip_existing: bool = True
    ) -> None:
        """Create a remote client."""
        auth_resp = await self._request(
            "post",
            "https://my.rainmachine.com/login/auth",
            json={"user": {"email": email, "pwd": password, "remember": 1}},
        )

        access_token: str = auth_resp["access_token"]

        sprinklers_resp = await self._request(
            "post",
            "https://my.rainmachine.com/devices/get-sprinklers",
            access_token=access_token,
            json={"user": {"email": email, "pwd": password, "remember": 1}},
        )

        for sprinkler in sprinklers_resp["sprinklers"]:
            if skip_existing and sprinkler["mac"] in self.controllers:
                continue

            controller: RemoteController = RemoteController(self._request)
            await controller.login(access_token, sprinkler["sprinklerId"], password)

            version_data = await controller.api.versions()
            controller.api_version = version_data["apiVer"]
            controller.hardware_version = version_data["hwVer"]
            controller.mac = sprinkler["mac"]
            controller.name = sprinkler["name"]
            controller.software_version = version_data["swVer"]

            self.controllers[sprinkler["mac"]] = controller class RequestError(RainMachineError):
    """Define an error related to invalid requests."""

    pass class TokenExpiredError(RainMachineError):
    """Define an error for expired access tokens that can't be refreshed."""

    pass TEST_ACCESS_TOKEN = "12345abcdef" TEST_API_VERSION = "4.5.0" TEST_EMAIL = "user@host.com" TEST_HOST = "192.168.1.100" TEST_HW_VERSION = 3 TEST_MAC = "ab:cd:ef:12:34:56" TEST_NAME = "My House" TEST_PASSWORD = "the_password_123" TEST_PORT = 8081 TEST_SW_VERSION = "4.0.925" def load_fixture(filename):
    """Load a fixture."""
    path = os.path.join(os.path.dirname(__file__), "fixtures", filename)
    with open(path, encoding="utf-8") as fptr:
        return fptr.read()
import asyncio
import aiohttp
import pytest
import tests.async_mock as mock
from datetime import datetime, timedelta
from regenmaschine import Client
from regenmaschine.errors import RequestError, TokenExpiredError
from tests.common import (
    TEST_ACCESS_TOKEN,
    TEST_API_VERSION,
    TEST_EMAIL,
    TEST_HOST,
    TEST_HW_VERSION,
    TEST_MAC,
    TEST_NAME,
    TEST_PASSWORD,
    TEST_PORT,
    TEST_SW_VERSION,
    load_fixture,
)
"""Define tests for the client object."""
# pylint: disable=protected-access





@pytest.mark.asyncio
async def test_legacy_login(authenticated_local_client):
    """Test loading a local client through the legacy method."""
    async with authenticated_local_client:
        async with aiohttp.ClientSession() as session:
            client = Client(session=session)
            await client.load_local(TEST_HOST, TEST_PASSWORD, port=TEST_PORT, ssl=False)
            controller = next(iter(client.controllers.values()))

            assert controller._access_token == TEST_ACCESS_TOKEN
            assert controller.api_version == TEST_API_VERSION

            assert controller.hardware_version == TEST_HW_VERSION