Initial Commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""PyNest module."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,373 @@
|
||||
"""PyNest API Client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from random import randint
|
||||
from types import TracebackType
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout, ContentTypeError, FormData
|
||||
|
||||
from .const import (
|
||||
APP_LAUNCH_URL_FORMAT,
|
||||
DEFAULT_NEST_ENVIRONMENT,
|
||||
NEST_AUTH_URL_JWT,
|
||||
NEST_REQUEST,
|
||||
TOKEN_URL,
|
||||
USER_AGENT,
|
||||
)
|
||||
from .exceptions import (
|
||||
BadCredentialsException,
|
||||
BadGatewayException,
|
||||
EmptyResponseException,
|
||||
GatewayTimeoutException,
|
||||
NotAuthenticatedException,
|
||||
PynestException,
|
||||
)
|
||||
from .models import (
|
||||
Bucket,
|
||||
FirstDataAPIResponse,
|
||||
GoogleAuthResponse,
|
||||
GoogleAuthResponseForCookies,
|
||||
NestAuthResponse,
|
||||
NestEnvironment,
|
||||
NestResponse,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
|
||||
class NestClient:
|
||||
"""Interface class for the Nest API."""
|
||||
|
||||
nest_session: NestResponse | None = None
|
||||
auth: GoogleAuthResponseForCookies | None = None
|
||||
session: ClientSession
|
||||
transport_url: str | None = None
|
||||
environment: NestEnvironment
|
||||
|
||||
# Legacy Auth
|
||||
refresh_token: str | None = None
|
||||
# Cookie Auth
|
||||
cookies: str | None = None
|
||||
issue_token: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: ClientSession | None = None,
|
||||
# refresh_token: str | None = None,
|
||||
# issue_token: str | None = None,
|
||||
# cookies: str | None = None,
|
||||
environment: NestEnvironment = DEFAULT_NEST_ENVIRONMENT,
|
||||
) -> None:
|
||||
"""Initialize NestClient."""
|
||||
|
||||
self.session = session or ClientSession()
|
||||
# self.refresh_token = refresh_token
|
||||
# self.issue_token = issue_token
|
||||
# self.cookies = cookies
|
||||
self.environment = environment
|
||||
|
||||
async def __aenter__(self) -> NestClient:
|
||||
"""__aenter__."""
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
"""__aexit__."""
|
||||
await self.session.close()
|
||||
|
||||
async def get_access_token(self) -> GoogleAuthResponse:
|
||||
"""Get a Nest access token."""
|
||||
|
||||
if self.refresh_token:
|
||||
await self.get_access_token_from_refresh_token(self.refresh_token)
|
||||
elif self.issue_token and self.cookies:
|
||||
await self.get_access_token_from_cookies(self.issue_token, self.cookies)
|
||||
|
||||
return self.auth
|
||||
|
||||
async def get_access_token_from_refresh_token(
|
||||
self, refresh_token: str | None = None
|
||||
) -> GoogleAuthResponse:
|
||||
"""Get a Nest refresh token from an authorization code."""
|
||||
|
||||
if refresh_token:
|
||||
self.refresh_token = refresh_token
|
||||
|
||||
if not self.refresh_token:
|
||||
raise Exception("No refresh token")
|
||||
|
||||
async with self.session.post(
|
||||
TOKEN_URL,
|
||||
data=FormData(
|
||||
{
|
||||
"refresh_token": self.refresh_token,
|
||||
"client_id": self.environment.client_id,
|
||||
"grant_type": "refresh_token",
|
||||
}
|
||||
),
|
||||
headers={
|
||||
"User-Agent": USER_AGENT,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
|
||||
if "error" in result:
|
||||
if result["error"] == "invalid_grant":
|
||||
raise BadCredentialsException(result["error"])
|
||||
|
||||
raise Exception(result["error"])
|
||||
|
||||
self.auth = GoogleAuthResponse(**result)
|
||||
|
||||
return self.auth
|
||||
|
||||
async def get_access_token_from_cookies(
|
||||
self, issue_token: str, cookies: str
|
||||
) -> GoogleAuthResponse:
|
||||
"""Get a Nest refresh token from an issue token and cookies."""
|
||||
|
||||
if issue_token:
|
||||
self.issue_token = issue_token
|
||||
|
||||
if cookies:
|
||||
self.cookies = cookies
|
||||
|
||||
async with self.session.get(
|
||||
issue_token,
|
||||
headers={
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": USER_AGENT,
|
||||
"X-Requested-With": "XmlHttpRequest",
|
||||
"Referer": "https://accounts.google.com/o/oauth2/iframe",
|
||||
"cookie": cookies,
|
||||
},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
|
||||
if "error" in result:
|
||||
# Cookie method
|
||||
if result["error"] == "USER_LOGGED_OUT":
|
||||
raise BadCredentialsException(
|
||||
f"{result['error']} - {result['detail']}"
|
||||
)
|
||||
|
||||
raise Exception(result["error"])
|
||||
|
||||
self.auth = GoogleAuthResponseForCookies(**result)
|
||||
|
||||
return self.auth
|
||||
|
||||
async def authenticate(self, access_token: str) -> NestResponse:
|
||||
"""Start a new Nest session with an access token."""
|
||||
|
||||
async with self.session.post(
|
||||
NEST_AUTH_URL_JWT,
|
||||
data=FormData(
|
||||
{
|
||||
"embed_google_oauth_access_token": True,
|
||||
"expire_after": "3600s",
|
||||
"google_oauth_access_token": access_token,
|
||||
"policy_id": "authproxy-oauth-policy",
|
||||
}
|
||||
),
|
||||
headers={
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"User-Agent": USER_AGENT,
|
||||
"Referer": self.environment.host,
|
||||
},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
nest_auth = NestAuthResponse(**result)
|
||||
|
||||
async with self.session.get(
|
||||
self.environment.host + "/session",
|
||||
headers={
|
||||
"Authorization": f"Basic {nest_auth.jwt}",
|
||||
"cookie": "G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5; cztoken="
|
||||
+ (nest_auth.jwt or ""),
|
||||
},
|
||||
) as response:
|
||||
try:
|
||||
nest_response = await response.json()
|
||||
except ContentTypeError as exception:
|
||||
nest_response = await response.text()
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while authenticating - {nest_response}. Please create an issue on GitHub."
|
||||
) from exception
|
||||
|
||||
# Change variable names since Python cannot handle vars that start with a number
|
||||
if nest_response.get("2fa_state"):
|
||||
nest_response["_2fa_state"] = nest_response.pop("2fa_state")
|
||||
if nest_response.get("2fa_enabled"):
|
||||
nest_response["_2fa_enabled"] = nest_response.pop("2fa_enabled")
|
||||
if nest_response.get("2fa_state_changed"):
|
||||
nest_response["_2fa_state_changed"] = nest_response.pop(
|
||||
"2fa_state_changed"
|
||||
)
|
||||
|
||||
if nest_response.get("error"):
|
||||
_LOGGER.error("Authentication error: %s", nest_response.get("error"))
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while authenticating - {nest_response}."
|
||||
)
|
||||
|
||||
try:
|
||||
self.nest_session = NestResponse(**nest_response)
|
||||
except Exception as exception:
|
||||
nest_response = await response.text()
|
||||
|
||||
if result.get("error"):
|
||||
_LOGGER.exception("Could not interpret Nest response")
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while authenticating - {nest_response}. Please create an issue on GitHub."
|
||||
) from exception
|
||||
|
||||
return self.nest_session
|
||||
|
||||
async def get_first_data(
|
||||
self, nest_access_token: str, user_id: str, request: dict = NEST_REQUEST
|
||||
) -> FirstDataAPIResponse:
|
||||
"""Get first data."""
|
||||
async with self.session.post(
|
||||
APP_LAUNCH_URL_FORMAT.format(host=self.environment.host, user_id=user_id),
|
||||
json=request,
|
||||
headers={
|
||||
"Authorization": f"Basic {nest_access_token}",
|
||||
"X-nl-user-id": user_id,
|
||||
"X-nl-protocol-version": str(1),
|
||||
},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
|
||||
if "2fa_enabled" in result:
|
||||
result["_2fa_enabled"] = result.pop("2fa_enabled")
|
||||
|
||||
if result.get("error"):
|
||||
_LOGGER.debug(
|
||||
"Received error from Nest service: %s", await response.text()
|
||||
)
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while subscribing - {result}"
|
||||
)
|
||||
|
||||
result = FirstDataAPIResponse(**result)
|
||||
|
||||
self.transport_url = result.service_urls["urls"]["transport_url"]
|
||||
|
||||
return result
|
||||
|
||||
async def subscribe_for_data(
|
||||
self,
|
||||
nest_access_token: str,
|
||||
user_id: str,
|
||||
transport_url: str,
|
||||
updated_buckets: dict,
|
||||
) -> Any:
|
||||
"""Subscribe for data."""
|
||||
timeout = 600
|
||||
|
||||
objects = []
|
||||
for bucket in updated_buckets:
|
||||
bucket = cast(Bucket, bucket)
|
||||
objects.append(
|
||||
{
|
||||
"object_key": bucket.object_key,
|
||||
"object_revision": bucket.object_revision,
|
||||
"object_timestamp": bucket.object_timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
# TODO throw better exceptions
|
||||
async with self.session.post(
|
||||
f"{transport_url}/v6/subscribe",
|
||||
timeout=ClientTimeout(total=timeout),
|
||||
json={
|
||||
"objects": objects,
|
||||
# "timeout": timeout,
|
||||
# "sessionID": f"ios-${user_id}.{random}.{epoch}",
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Basic {nest_access_token}",
|
||||
"X-nl-user-id": user_id,
|
||||
"X-nl-protocol-version": str(1),
|
||||
},
|
||||
) as response:
|
||||
_LOGGER.debug("Data received via subscriber (status: %s)", response.status)
|
||||
|
||||
if response.status == 401:
|
||||
raise NotAuthenticatedException(await response.text())
|
||||
|
||||
if response.status == 504:
|
||||
raise GatewayTimeoutException(await response.text())
|
||||
|
||||
if response.status == 502:
|
||||
raise BadGatewayException(await response.text())
|
||||
|
||||
if response.status == 200 and response.content_type == "text/plain":
|
||||
raise EmptyResponseException(await response.text())
|
||||
|
||||
try:
|
||||
result = await response.json()
|
||||
except ContentTypeError as error:
|
||||
result = await response.text()
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while subscribing - {result}"
|
||||
) from error
|
||||
|
||||
# TODO type object
|
||||
return result
|
||||
|
||||
async def update_objects(
|
||||
self,
|
||||
nest_access_token: str,
|
||||
user_id: str,
|
||||
transport_url: str,
|
||||
objects_to_update: dict,
|
||||
) -> Any:
|
||||
"""Subscribe for data."""
|
||||
|
||||
epoch = int(time.time())
|
||||
random = str(randint(100, 999))
|
||||
|
||||
# TODO throw better exceptions
|
||||
async with self.session.post(
|
||||
f"{transport_url}/v6/put",
|
||||
json={
|
||||
"session": f"ios-${user_id}.{random}.{epoch}",
|
||||
"objects": objects_to_update,
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Basic {nest_access_token}",
|
||||
"X-nl-user-id": user_id,
|
||||
"X-nl-protocol-version": str(1),
|
||||
},
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
raise NotAuthenticatedException(await response.text())
|
||||
|
||||
try:
|
||||
result = await response.json()
|
||||
except ContentTypeError as err:
|
||||
result = await response.text()
|
||||
|
||||
raise PynestException(
|
||||
f"{response.status} error while subscribing - {result}"
|
||||
) from err
|
||||
|
||||
# TODO type object
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Constants used by PyNest."""
|
||||
|
||||
from .enums import BucketType, Environment
|
||||
from .models import NestEnvironment
|
||||
|
||||
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"
|
||||
|
||||
NEST_ENVIRONMENTS: dict[str, NestEnvironment] = {
|
||||
Environment.PRODUCTION: NestEnvironment(
|
||||
name="Google Account",
|
||||
client_id="733249279899-1gpkq9duqmdp55a7e5lft1pr2smumdla.apps.googleusercontent.com", # Nest iOS application
|
||||
host="https://home.nest.com",
|
||||
),
|
||||
Environment.FIELDTEST: NestEnvironment(
|
||||
name="Google Account (Field Test)",
|
||||
client_id="384529615266-57v6vaptkmhm64n9hn5dcmkr4at14p8j.apps.googleusercontent.com", # Test Flight Beta Nest iOS application
|
||||
host="https://home.ft.nest.com",
|
||||
),
|
||||
}
|
||||
|
||||
DEFAULT_NEST_ENVIRONMENT = NEST_ENVIRONMENTS[Environment.PRODUCTION]
|
||||
|
||||
# / URL for refresh token generation
|
||||
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
|
||||
# App launch API endpoint
|
||||
APP_LAUNCH_URL_FORMAT = "{host}/api/0.1/user/{user_id}/app_launch"
|
||||
NEST_AUTH_URL_JWT = "https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt"
|
||||
|
||||
NEST_REQUEST = {
|
||||
"known_bucket_types": [
|
||||
BucketType.KRYPTONITE,
|
||||
BucketType.STRUCTURE,
|
||||
BucketType.TOPAZ,
|
||||
BucketType.WHERE,
|
||||
BucketType.USER,
|
||||
],
|
||||
"known_bucket_versions": [],
|
||||
}
|
||||
|
||||
FULL_NEST_REQUEST = {
|
||||
"known_bucket_types": [
|
||||
BucketType.BUCKETS,
|
||||
BucketType.METADATA,
|
||||
BucketType.KRYPTONITE,
|
||||
BucketType.STRUCTURE,
|
||||
BucketType.TOPAZ,
|
||||
BucketType.WHERE,
|
||||
BucketType.USER,
|
||||
BucketType.DEMAND_RESPONSE,
|
||||
BucketType.WIDGET_TRACK,
|
||||
BucketType.OCCUPANCY,
|
||||
BucketType.MESSAGE,
|
||||
BucketType.MESSAGE_CENTER,
|
||||
BucketType.LINK,
|
||||
BucketType.SAFETY,
|
||||
BucketType.SAFETY_SUMMARY,
|
||||
BucketType.DEVICE_ALERT_DIALOG,
|
||||
BucketType.QUARTZ,
|
||||
BucketType.TOPAZ_RESOURCE,
|
||||
BucketType.TRACK,
|
||||
BucketType.TRIP,
|
||||
BucketType.STRUCTURE_METADATA,
|
||||
BucketType.USER,
|
||||
BucketType.WIDGET_TRACK,
|
||||
],
|
||||
"known_bucket_versions": [],
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Enums for Nest Protect."""
|
||||
|
||||
import logging
|
||||
from enum import StrEnum, unique
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@unique
|
||||
class BucketType(StrEnum):
|
||||
"""Bucket types."""
|
||||
|
||||
BUCKETS = "buckets"
|
||||
DELAYED_TOPAZ = "delayed_topaz"
|
||||
DEMAND_RESPONSE = "demand_response"
|
||||
DEVICE = "device"
|
||||
DEVICE_ALERT_DIALOG = "device_alert_dialog"
|
||||
GEOFENCE_INFO = "geofence_info"
|
||||
KRYPTONITE = "kryptonite" # Temperature Sensors
|
||||
LINK = "link"
|
||||
MESSAGE = "message"
|
||||
MESSAGE_CENTER = "message_center"
|
||||
METADATA = "metadata"
|
||||
OCCUPANCY = "occupancy"
|
||||
QUARTZ = "quartz"
|
||||
RCS_SETTINGS = "rcs_settings"
|
||||
SAFETY = "safety"
|
||||
SAFETY_SUMMARY = "safety_summary"
|
||||
SCHEDULE = "schedule"
|
||||
SHARED = "shared"
|
||||
STRUCTURE = "structure" # General
|
||||
STRUCTURE_HISTORY = "structure_history"
|
||||
STRUCTURE_METADATA = "structure_metadata"
|
||||
TOPAZ = "topaz" # Nest Protect
|
||||
TOPAZ_RESOURCE = "topaz_resource"
|
||||
TRACK = "track"
|
||||
TRIP = "trip"
|
||||
TUNEUPS = "tuneups"
|
||||
USER = "user"
|
||||
USER_ALERT_DIALOG = "user_alert_dialog"
|
||||
USER_SETTINGS = "user_settings"
|
||||
WIDGET_TRACK = "widget_track"
|
||||
WHERE = "where" # Areas
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value): # type: ignore[override]
|
||||
_LOGGER.warning("Unsupported value %s has been returned for %s", value, cls)
|
||||
|
||||
return cls.UNKNOWN
|
||||
|
||||
|
||||
@unique
|
||||
class Environment(StrEnum):
|
||||
"""Bucket types."""
|
||||
|
||||
FIELDTEST = "fieldtest"
|
||||
PRODUCTION = "production"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Exceptions used by PyNest."""
|
||||
|
||||
|
||||
class PynestException(Exception):
|
||||
"""Base class for all exceptions raised by pynest."""
|
||||
|
||||
|
||||
class NestServiceException(Exception):
|
||||
"""Raised when service is not available."""
|
||||
|
||||
|
||||
class BadCredentialsException(Exception):
|
||||
"""Raised when credentials are incorrect."""
|
||||
|
||||
|
||||
class NotAuthenticatedException(Exception):
|
||||
"""Raised when session is invalid."""
|
||||
|
||||
|
||||
class GatewayTimeoutException(NestServiceException):
|
||||
"""Raised when server times out."""
|
||||
|
||||
|
||||
class BadGatewayException(NestServiceException):
|
||||
"""Raised when server returns Bad Gateway."""
|
||||
|
||||
|
||||
class EmptyResponseException(NestServiceException):
|
||||
"""Raised when server returns Status 200 (OK), but empty response."""
|
||||
@@ -0,0 +1,348 @@
|
||||
"""Models used by PyNest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .enums import BucketType
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestLimits:
|
||||
"""Nest Limits."""
|
||||
|
||||
thermostats_per_structure: int
|
||||
structures: int
|
||||
smoke_detectors_per_structure: int
|
||||
smoke_detectors: int
|
||||
thermostats: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestUrls:
|
||||
"""Nest Urls."""
|
||||
|
||||
rubyapi_url: str
|
||||
czfe_url: str
|
||||
log_upload_url: str
|
||||
transport_url: str
|
||||
weather_url: str
|
||||
support_url: str
|
||||
direct_transport_url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestResponse:
|
||||
"""Class that reflects a Nest API response."""
|
||||
|
||||
access_token: float
|
||||
email: str
|
||||
expires_in: str
|
||||
userid: str
|
||||
is_superuser: bool
|
||||
language: str
|
||||
weave: dict[str, str]
|
||||
user: str
|
||||
is_staff: bool
|
||||
error: dict | None = None
|
||||
urls: NestUrls = field(default_factory=NestUrls)
|
||||
limits: NestLimits = field(default_factory=NestLimits)
|
||||
|
||||
_2fa_state: str = None
|
||||
_2fa_enabled: bool = None
|
||||
_2fa_state_changed: str = None
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if session is expired."""
|
||||
# Tue, 01-Mar-2022 23:15:55 GMT
|
||||
expiry_date = datetime.datetime.strptime(
|
||||
self.expires_in, "%a, %d-%b-%Y %H:%M:%S %Z"
|
||||
)
|
||||
return expiry_date <= datetime.datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bucket:
|
||||
"""Class that reflects a Nest API response."""
|
||||
|
||||
object_key: str
|
||||
object_revision: int
|
||||
object_timestamp: int
|
||||
# value: dict[str, Any]
|
||||
value: dict[str, Any] | TopazBucketValue | WhereBucketValue
|
||||
type: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Set the bucket type during post init."""
|
||||
self.type = BucketType(self.object_key.split(".")[0])
|
||||
|
||||
# if self.type == BucketType.TOPAZ:
|
||||
# self.value = TopazBucketValue(**self.value)
|
||||
if self.type == BucketType.WHERE:
|
||||
if isinstance(self.value, WhereBucketValue):
|
||||
# It's already the correct type, no need to reinitialize
|
||||
pass
|
||||
else:
|
||||
# Convert dictionary to WhereBucketValue instance
|
||||
self.value = WhereBucketValue(**self.value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Where:
|
||||
"""TODO."""
|
||||
|
||||
name: str
|
||||
where_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BucketValue:
|
||||
"""Nest Protect values."""
|
||||
|
||||
# def __iter__(self):
|
||||
# return (getattr(self, field.name) for field in fields(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhereBucketValue(BucketValue):
|
||||
"""Nest Protect values."""
|
||||
|
||||
wheres: list[Where] = field(default_factory=Where)
|
||||
|
||||
def __post_init__(self):
|
||||
"""TODO."""
|
||||
self.wheres = [Where(**w) for w in self.wheres] if self.wheres else []
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhereBucket(Bucket):
|
||||
"""Class that reflects a Nest API response."""
|
||||
|
||||
object_key: str
|
||||
object_revision: str
|
||||
object_timestamp: str
|
||||
value: WhereBucketValue = field(default_factory=WhereBucketValue)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TopazBucketValue(BucketValue):
|
||||
"""Nest Protect values."""
|
||||
|
||||
spoken_where_id: str
|
||||
creation_time: int
|
||||
installed_locale: str
|
||||
ntp_green_led_brightness: int
|
||||
component_buzzer_test_passed: bool
|
||||
wifi_ip_address: str
|
||||
wired_led_enable: bool
|
||||
wifi_regulatory_domain: str
|
||||
co_blame_duration: int
|
||||
is_rcs_capable: bool
|
||||
fabric_id: str
|
||||
battery_health_state: int
|
||||
steam_detection_enable: bool
|
||||
hushed_state: bool
|
||||
capability_level: float
|
||||
home_alarm_link_type: int
|
||||
model: str
|
||||
component_smoke_test_passed: bool
|
||||
component_speaker_test_passed: bool
|
||||
removed_from_base: bool
|
||||
smoke_sequence_number: int
|
||||
home_away_input: bool
|
||||
device_locale: str
|
||||
co_blame_threshold: int
|
||||
kl_software_version: str
|
||||
component_us_test_passed: bool
|
||||
auto_away: bool
|
||||
night_light_enable: bool
|
||||
component_als_test_passed: bool
|
||||
speaker_test_results: 32768
|
||||
wired_or_battery: int
|
||||
is_rcs_used: bool
|
||||
replace_by_date_utc_secs: int
|
||||
certification_body: 2
|
||||
component_pir_test_passed: bool
|
||||
structure_id: str
|
||||
software_version: str
|
||||
component_hum_test_passed: bool
|
||||
home_alarm_link_capable: bool
|
||||
night_light_brightness: int
|
||||
device_external_color: str
|
||||
latest_manual_test_end_utc_secs: int
|
||||
smoke_status: int
|
||||
latest_manual_test_start_utc_secs: int
|
||||
component_temp_test_passed: bool
|
||||
home_alarm_link_connected: bool
|
||||
co_status: int
|
||||
heat_status: int
|
||||
product_id: int
|
||||
night_light_continuous: bool
|
||||
co_previous_peak: int
|
||||
auto_away_decision_time_secs: int
|
||||
component_co_test_passed: bool
|
||||
where_id: str
|
||||
serial_number: str
|
||||
component_heat_test_passed: bool
|
||||
latest_manual_test_cancelled: bool
|
||||
thread_mac_address: str
|
||||
resource_id: str
|
||||
buzzer_test_results: int
|
||||
wifi_mac_address: str
|
||||
line_power_present: bool
|
||||
gesture_hush_enable: bool
|
||||
device_born_on_date_utc_secs: int
|
||||
ntp_green_led_enable: bool
|
||||
component_led_test_passed: bool
|
||||
co_sequence_number: int
|
||||
thread_ip_address: list[str]
|
||||
component_wifi_test_passed: bool
|
||||
heads_up_enable: bool
|
||||
battery_level: int
|
||||
last_audio_self_test_end_utc_secs: int
|
||||
last_audio_self_test_start_utc_secs: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TopazBucket(Bucket):
|
||||
"""Class that reflects a Nest API response."""
|
||||
|
||||
value: TopazBucketValue = field(default_factory=TopazBucketValue)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleAuthResponse:
|
||||
"""Class that reflects a Google Auth response."""
|
||||
|
||||
access_token: str
|
||||
scope: str
|
||||
token_type: str
|
||||
expires_in: int
|
||||
id_token: str
|
||||
expiry_date: datetime.datetime = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Set the expiry date during post init."""
|
||||
self.expiry_date = datetime.datetime.now() + datetime.timedelta(
|
||||
seconds=self.expires_in
|
||||
)
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if access token is expired."""
|
||||
return self.expiry_date <= datetime.datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleAuthResponseForCookies(GoogleAuthResponse):
|
||||
"""Class that reflects a Google Auth response for cookies."""
|
||||
|
||||
login_hint: str
|
||||
session_state: dict[str, dict[str, str]] = field(default_factory=dict)
|
||||
|
||||
|
||||
# TODO rewrite to snake_case
|
||||
@dataclass
|
||||
class NestAuthClaims:
|
||||
"""TODO."""
|
||||
|
||||
subject: Any | None = None
|
||||
expirationTime: str | None = None
|
||||
policyId: str | None = None
|
||||
structureConstraint: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestAuthResponse:
|
||||
"""TODO."""
|
||||
|
||||
jwt: str | None = None
|
||||
claims: NestAuthClaims = field(default_factory=NestAuthClaims)
|
||||
error: dict | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestEnvironment:
|
||||
"""Class to describe a Nest environment."""
|
||||
|
||||
name: str
|
||||
client_id: str
|
||||
host: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Weather:
|
||||
"""TODO."""
|
||||
|
||||
icon: str
|
||||
sunrise: str
|
||||
sunset: str
|
||||
temp_c: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
"""TODO."""
|
||||
|
||||
city: str
|
||||
country: str
|
||||
state: str
|
||||
zip: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherForStructures:
|
||||
"""TODO."""
|
||||
|
||||
current: Weather
|
||||
location: Location
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceUrls:
|
||||
"""TODO."""
|
||||
|
||||
czfe_url: str
|
||||
direct_transport_url: str
|
||||
log_upload_url: str
|
||||
rubyapi_url: str
|
||||
support_url: str
|
||||
transport_url: str
|
||||
weather_url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Weave:
|
||||
"""TODO."""
|
||||
|
||||
access_token: str
|
||||
pairing_token: str
|
||||
service_config: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Limits:
|
||||
"""TODO."""
|
||||
|
||||
smoke_detectors: int
|
||||
smoke_detectors_per_structure: int
|
||||
structures: int
|
||||
thermostats: int
|
||||
thermostats_per_structure: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirstDataAPIResponse:
|
||||
"""TODO."""
|
||||
|
||||
weather_for_structures: dict[str, WeatherForStructures]
|
||||
service_urls: dict[str, ServiceUrls | Weave | Limits]
|
||||
_2fa_enabled: bool
|
||||
updated_buckets: list[Bucket]
|
||||
|
||||
def __post_init__(self):
|
||||
"""TODO."""
|
||||
self.updated_buckets = (
|
||||
[Bucket(**b) for b in self.updated_buckets] if self.updated_buckets else []
|
||||
)
|
||||
Reference in New Issue
Block a user