diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7d9b3ad..0a7eefd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -58,5 +58,7 @@ } }, "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12", - "name": "PyTado" + "name": "PyTado", + "postCreateCommand": "python3 -m venv venv && . venv/bin/activate && pip install --upgrade pip && pip install -e '.[all]' && pre-commit install", + "updateContentCommand": "python3 -m venv venv && . venv/bin/activate && pip install --upgrade pip && pip install -e '.[all]' && pre-commit install" } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d753d7..eaa6633 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,24 +26,29 @@ repos: exclude: tests/ args: ["--profile", "black"] -- repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.5.7 hooks: - - id: pyupgrade + - id: autopep8 + exclude: tests/ + args: [--max-line-length=100, --aggressive, --aggressive] - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: - id: flake8 exclude: tests/ - args: [--max-line-length=100 ] + args: [--max-line-length=100] + +- repo: https://github.com/asottile/pyupgrade + rev: v3.19.1 + hooks: + - id: pyupgrade - repo: https://github.com/PyCQA/bandit rev: '1.8.0' hooks: - id: bandit - exclude: tests/ - args: ["--skip", "B105"] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. diff --git a/PyTado/http.py b/PyTado/http.py index 5f98191..360be08 100644 --- a/PyTado/http.py +++ b/PyTado/http.py @@ -68,7 +68,7 @@ def __init__( device: int | None = None, mode: Mode = Mode.OBJECT, params: dict[str, Any] | None = None, - ): + ) -> None: self.endpoint = endpoint self.command = command self.action = action @@ -92,7 +92,7 @@ def __init__( device: int | None = None, mode: Mode = Mode.OBJECT, params: dict[str, Any] | None = None, - ): + ) -> None: super().__init__( endpoint=endpoint, command=command, @@ -113,7 +113,7 @@ def action(self) -> Action | str: return self._action @action.setter - def action(self, value: Action | str): + def action(self, value: Action | str) -> None: """Set request action""" self._action = value @@ -138,7 +138,7 @@ def __init__( password: str, http_session: requests.Session | None = None, debug: bool = False, - ): + ) -> None: if debug: _LOGGER.setLevel(logging.DEBUG) else: @@ -154,7 +154,11 @@ def __init__( self._x_api = self._check_x_line_generation() - def _log_response(self, response: requests.Response, *args, **kwargs): + @property + def is_x_line(self) -> bool: + return self._x_api + + def _log_response(self, response: requests.Response, *args, **kwargs) -> None: og_request_method = response.request.method og_request_url = response.request.url og_request_headers = response.request.headers @@ -172,12 +176,10 @@ def request(self, request: TadoRequest) -> dict[str, Any]: self._refresh_token() headers = self._headers - data = self._configure_payload(headers, request) - url = self._configure_url(request) - http_request = requests.Request(request.action, url, headers=headers, data=data) + http_request = requests.Request(method=request.action, url=url, headers=headers, data=data) prepped = http_request.prepare() retries = _DEFAULT_RETRIES @@ -201,17 +203,13 @@ def request(self, request: TadoRequest) -> dict[str, Any]: _DEFAULT_RETRIES, e, ) - raise e + raise TadoException(e) if response.text is None or response.text == "": return {} return response.json() - @property - def is_x_line(self): - return self._x_api - def _configure_url(self, request: TadoRequest) -> str: if request.endpoint == Endpoint.MOBILE: url = f"{request.endpoint}{request.command}" @@ -242,6 +240,7 @@ def _configure_payload(self, headers: dict[str, str], request: TadoRequest) -> b return json.dumps(request.payload).encode("utf8") def _set_oauth_header(self, data: dict[str, Any]) -> str: + """Set the OAuth header and return the refresh token""" access_token = data["access_token"] expires_in = float(data["expires_in"]) @@ -250,15 +249,15 @@ def _set_oauth_header(self, data: dict[str, Any]) -> str: self._token_refresh = refresh_token self._refresh_at = datetime.now() self._refresh_at = self._refresh_at + timedelta(seconds=expires_in) - # we subtract 30 seconds from the correct refresh time - # then we have a 30 seconds timespan to get a new refresh_token + # We subtract 30 seconds from the correct refresh time. + # Then we have a 30 seconds timespan to get a new refresh_token self._refresh_at = self._refresh_at - timedelta(seconds=30) - self._headers["Authorization"] = "Bearer " + access_token + self._headers["Authorization"] = f"Bearer {access_token}" return refresh_token def _refresh_token(self) -> None: - + """Refresh the token if it is about to expire""" if self._refresh_at >= datetime.now(): return @@ -274,52 +273,58 @@ def _refresh_token(self) -> None: self._session = requests.Session() self._session.hooks["response"].append(self._log_response) - response = self._session.request( - "post", - url, - params=data, - timeout=_DEFAULT_TIMEOUT, - data=json.dumps({}).encode("utf8"), - headers={ - "Content-Type": "application/json", - "Referer": "https://app.tado.com/", - }, - ) + try: + response = self._session.request( + "post", + url, + params=data, + timeout=_DEFAULT_TIMEOUT, + data=json.dumps({}).encode("utf8"), + headers={ + "Content-Type": "application/json", + "Referer": "https://app.tado.com/", + }, + ) + except requests.exceptions.ConnectionError as e: + _LOGGER.error("Connection error: %s", e) + raise TadoException(e) - if response.status_code == 200: - self._set_oauth_header(response.json()) - return + if response.status_code != 200: + raise TadoWrongCredentialsException( + "Failed to refresh token, probably wrong credentials. " + f"Status code: {response.status_code}" + ) - raise TadoException( - f"Unknown error while refreshing token with status code {response.status_code}" - ) + self._set_oauth_header(response.json()) def _login(self) -> tuple[int, str]: - - headers = self._headers - headers["Content-Type"] = "application/json" + """Login to the API and get the refresh token""" url = "https://auth.tado.com/oauth/token" data = { - "client_id": "tado-web-app", - "client_secret": "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, "grant_type": "password", "password": self._password, "scope": "home.user", "username": self._username, } - response = self._session.request( - "post", - url, - params=data, - timeout=_DEFAULT_TIMEOUT, - data=json.dumps({}).encode("utf8"), - headers={ - "Content-Type": "application/json", - "Referer": "https://app.tado.com/", - }, - ) + try: + response = self._session.request( + "post", + url, + params=data, + timeout=_DEFAULT_TIMEOUT, + data=json.dumps({}).encode("utf8"), + headers={ + "Content-Type": "application/json", + "Referer": "https://app.tado.com/", + }, + ) + except requests.exceptions.ConnectionError as e: + _LOGGER.error("Connection error: %s", e) + raise TadoException(e) if response.status_code == 400: raise TadoWrongCredentialsException("Your username or password is invalid")