1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
|
"""API interface base class."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
import random
from typing import Any, cast
from aiohttp import ClientResponse, ClientSession, ClientTimeout
from .consts import API_URL, BAMKEY, LOGIN_KEY, TIMEOUT
from .exceptions import (
SleepIQAPIException,
SleepIQLoginException,
SleepIQTimeoutException,
)
SOURCE_APP = "AsyncSleepIQ API"
def random_user_agent() -> str:
"""Create a randomly generated sorta valid User Agent string."""
uas = {
"Edge": ("AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/98.0.4758.80 Safari/537.36 Edg/98.0.1108.43"),
"Chrome": ("AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/97.0.4692.99 Safari/537.36"),
"Firefox": "Gecko/20100101 Firefox/96.0",
"iphone": ("AppleWebKit/605.1.15 (KHTML, like Gecko) " "Version/15.2 Mobile/15E148 Safari/604.1"),
"Safari": ("AppleWebKit/605.1.15 (KHTML, like Gecko) " "Version/11.1.2 Safari/605.1.15"),
}
os = {
"windows": "Windows NT 10.0; Win64; x64",
"iphone": "iPhone; CPU iPhone OS 15_2_1 like Mac OS X",
"mac": "Macintosh; Intel Mac OS X 10_11_6",
}
template = "Mozilla/5.0 ({os}) {ua}"
return template.format(os=random.choice(list(os.values())), ua=random.choice(list(uas.values())))
class SleepIQAPI:
"""API interface base class."""
def __init__(
self,
email: str | None = None,
password: str | None = None,
login_method: int = LOGIN_KEY,
client_session: ClientSession | None = None,
) -> None:
"""Initialize AsyncSleepIQ API Interface."""
self.email = email
self.password = password
self.key = ""
self._session = client_session or ClientSession()
self._headers = {"User-Agent": random_user_agent()}
self._login_method = login_method
self._account_id = ""
async def close_session(self) -> None:
"""Close the API session."""
if self._session:
await self._session.close()
async def login(self, email: str | None = None, password: str | None = None) -> None:
"""Login using the with the email/password provided or stored."""
if not email:
email = self.email
if not password:
password = self.password
if not email or not password:
raise SleepIQLoginException("username/password not set")
try:
if self._login_method == LOGIN_KEY:
await self.login_key(email, password)
else:
await self.login_cookie(email, password)
except asyncio.TimeoutError as ex:
# timed out
raise SleepIQTimeoutException("API call timed out") from ex
except SleepIQTimeoutException as ex:
raise ex
except Exception as ex:
raise SleepIQLoginException(f"Connection failure: {ex}") from ex
# store in case we need to login again
self.email = email
self.password = password
async def login_key(self, email: str, password: str) -> None:
"""Login using the key authentication method with the email/password provided."""
self.key = ""
auth_data = {"login": email, "password": password}
async with self._session.put(
API_URL + "/login", headers=self._headers, timeout=TIMEOUT, json=auth_data
) as resp:
if resp.status == 401:
raise SleepIQLoginException("Incorrect username or password")
if resp.status == 403:
raise SleepIQLoginException("User Agent is blocked. May need to update GenUserAgent data?")
if resp.status not in (200, 201):
raise SleepIQLoginException(
"Unexpected response code: {code}\n{body}".format(
code=resp.status,
body=resp.text,
)
)
json = await resp.json()
self.key = json["key"]
async def login_cookie(self, email: str, password: str) -> None:
"""Login using the cookie authentication method with the email/password provided."""
auth_data = {
"Email": email,
"Password": password,
"ClientID": "2oa5825venq9kek1dnrhfp7rdh",
}
async with self._session.post(
"https://l06it26kuh.execute-api.us-east-1.amazonaws.com/Prod/v1/token",
headers=self._headers,
timeout=TIMEOUT,
json=auth_data,
) as resp:
if resp.status == 401:
raise SleepIQLoginException("Incorrect username or password")
if resp.status == 403:
raise SleepIQLoginException("User Agent is blocked. May need to update GenUserAgent data?")
if resp.status not in (200, 201):
raise SleepIQLoginException(
"Unexpected response code: {code}\n{body}".format(
code=resp.status,
body=resp.text,
)
)
json = await resp.json()
token = json["data"]["AccessToken"]
self._headers["Authorization"] = token
async with self._session.get(API_URL + "/user/jwt", headers=self._headers, timeout=TIMEOUT) as resp:
if resp.status not in (200, 201):
raise SleepIQLoginException(
"Unexpected response code: {code}\n{body}".format(
code=resp.status,
body=resp.text,
)
)
async def put(self, url: str, json: dict[str, Any] = {}, params: dict[str, Any] = {}) -> dict[str, Any]:
"""Make a PUT request to the API."""
return await self.__make_request(self._session.put, url, json, params)
async def get(self, url: str, json: dict[str, Any] = {}, params: dict[str, Any] = {}) -> dict[str, Any] | Any:
"""Make a GET request to the API."""
return await self.__make_request(self._session.get, url, json, params)
async def check(self, url: str, json: dict[str, Any] = {}, params: dict[str, Any] = {}) -> bool:
"""Check if a GET request to the API would be successful."""
return cast(
bool,
await self.__make_request(self._session.get, url, json, params, check=True),
)
async def bamkey(self, bed_id: str, key: str, args: list[str] = []) -> str:
"""Make a request to the API using the bamkey endpoint."""
url = f"sn/v1/accounts/{self._account_id}/beds/{bed_id}/bamkey"
json = {
"args": " ".join(args),
"key": BAMKEY[key],
"sourceApplication": SOURCE_APP,
}
json = await self.put(url, json)
return json.get("cdcResponse", "").replace("PASS:", "")
async def __make_request(
self,
make_request: Callable[..., AbstractAsyncContextManager[ClientResponse]],
url: str,
json: dict[str, Any] = {},
params: dict[str, Any] = {},
retry: bool = True,
check: bool = False,
) -> bool | dict[str, Any] | Any:
"""Make a request to the API."""
timeout = ClientTimeout(total=TIMEOUT)
params["_k"] = self.key
try:
async with make_request(
API_URL + "/" + url,
headers=self._headers,
timeout=timeout,
json=json,
params=params,
) as resp:
if check:
return resp.status == 200
if resp.status != 200:
if retry and resp.status in (401, 404):
# login and try again
await self.login()
return await self.__make_request(make_request, url, json, params, False)
raise SleepIQAPIException(resp.status, f"API call error response {resp.status}\n{resp.text}")
return await resp.json()
except asyncio.TimeoutError as ex:
# timed out
raise SleepIQTimeoutException("API call timed out") from ex
|