Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/tadoasync/models_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
51 changes: 51 additions & 0 deletions src/tadoasync/tadoasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
import enum
import logging
import re
import time
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
Expand Down Expand Up @@ -127,6 +128,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:
Expand Down Expand Up @@ -154,6 +159,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:
Expand Down Expand Up @@ -627,6 +661,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(
Expand Down Expand Up @@ -809,6 +845,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()
Expand Down
Loading