File: api.py

package info (click to toggle)
python-asyncsleepiq 1.5.3-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 192 kB
  • sloc: python: 1,047; makefile: 4
file content (211 lines) | stat: -rw-r--r-- 8,531 bytes parent folder | download
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