diff --git a/src/tadoasync/models.py b/src/tadoasync/models.py index e11d880..c62bc81 100644 --- a/src/tadoasync/models.py +++ b/src/tadoasync/models.py @@ -589,6 +589,8 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: """Pre deserialize hook.""" if not d["sensorDataPoints"]: d["sensorDataPoints"] = None + if d.get("nextTimeBlock") is None: + d["nextTimeBlock"] = {} return d diff --git a/src/tadoasync/tadoasync.py b/src/tadoasync/tadoasync.py index c1f1ad2..8cccb76 100644 --- a/src/tadoasync/tadoasync.py +++ b/src/tadoasync/tadoasync.py @@ -5,6 +5,7 @@ import asyncio import enum import logging +import re import time from dataclasses import dataclass from datetime import UTC, datetime, timedelta @@ -115,6 +116,10 @@ def __init__( self._device_activation_status = DeviceActivationStatus.NOT_STARTED self._expires_at: datetime | None = None + self._last_headers: dict[str, str] = {} + self._last_limit: int | None = None + self._last_remaining: int | None = None + _LOGGER.setLevel(logging.DEBUG if debug else logging.INFO) async def async_init(self) -> None: @@ -141,6 +146,35 @@ def refresh_token(self) -> str | None: """Return the refresh token.""" return self._refresh_token + @property + def session(self) -> ClientSession: + """Return the aiohttp session.""" + return self._ensure_session() + + @property + def home_id(self) -> int | None: + """Return the home ID.""" + return self._home_id + + @property + def access_token(self) -> str | None: + """Return the OAuth access token.""" + return self._access_token + + async def refresh_auth(self) -> None: + """Refresh the OAuth token.""" + await self._refresh_auth() + + @property + def last_headers(self) -> dict[str, str]: + """Return headers from the last API response.""" + return self._last_headers + + @property + def rate_limit(self) -> tuple[int | None, int | None]: + """Return rate limit (limit, remaining) from the last API response.""" + return (self._last_limit, self._last_remaining) + async def login_device_flow(self) -> DeviceActivationStatus: """Login using device flow.""" if self._device_activation_status != DeviceActivationStatus.NOT_STARTED: @@ -586,6 +620,8 @@ async def _request( request = await session.request( method=method.value, url=str(url), headers=headers, json=data ) + self._last_headers = dict(request.headers) + self._last_limit, self._last_remaining = self._parse_rate_limit() request.raise_for_status() except TimeoutError as err: raise TadoConnectionError( @@ -770,6 +806,21 @@ def _ensure_session(self) -> ClientSession: self._close_session = True return self._session + def _parse_rate_limit(self) -> tuple[int | None, int | None]: + """Parse rate limit from RateLimit headers.""" + + def extract(pattern: str, value: str) -> int | None: + match = re.search(pattern, value) + return int(match.group(1)) if match else None + + policy = self._last_headers.get("RateLimit-Policy", "") + rl = self._last_headers.get("RateLimit", "") + + limit = extract(r"q=(\d+)", policy) + remaining = extract(r"r=(\d+)", rl) + + return limit, remaining + async def __aenter__(self) -> Self: """Async enter.""" await self.async_init()