aioamazondevices 0.8.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioamazondevices
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Python library to control Amazon devices
5
5
  Home-page: https://github.com/chemelli74/aioamazondevices
6
6
  License: Apache-2.0
@@ -20,6 +20,8 @@ Requires-Dist: beautifulsoup4
20
20
  Requires-Dist: colorlog
21
21
  Requires-Dist: httpx
22
22
  Requires-Dist: orjson
23
+ Requires-Dist: pyasn1
24
+ Requires-Dist: rsa
23
25
  Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
24
26
  Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
25
27
  Project-URL: Repository, https://github.com/chemelli74/aioamazondevices
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aioamazondevices"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  description = "Python library to control Amazon devices"
5
5
  authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
6
6
  license = "Apache-2.0"
@@ -27,10 +27,12 @@ beautifulsoup4 = "*"
27
27
  colorlog = "*"
28
28
  httpx = "*"
29
29
  orjson = "*"
30
+ pyasn1 = "*"
31
+ rsa = "*"
30
32
 
31
33
  [tool.poetry.group.dev.dependencies]
32
34
  pytest = "^8.1"
33
- pytest-cov = "^5.0"
35
+ pytest-cov = ">=5,<7"
34
36
 
35
37
  [tool.semantic_release]
36
38
  version_toml = ["pyproject.toml:tool.poetry.version"]
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.10.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -15,8 +15,9 @@ from urllib.parse import parse_qs, urlencode
15
15
 
16
16
  import orjson
17
17
  from bs4 import BeautifulSoup, Tag
18
- from httpx import URL, AsyncClient, Response
18
+ from httpx import URL, AsyncClient, Auth, Response
19
19
 
20
+ from .auth import Authenticator
20
21
  from .const import (
21
22
  _LOGGER,
22
23
  AMAZON_APP_BUNDLE_ID,
@@ -28,13 +29,14 @@ from .const import (
28
29
  AMAZON_DEVICE_TYPE,
29
30
  DEFAULT_ASSOC_HANDLE,
30
31
  DEFAULT_HEADERS,
32
+ DEVICES,
31
33
  DOMAIN_BY_COUNTRY,
32
34
  HTML_EXTENSION,
33
35
  JSON_EXTENSION,
34
36
  SAVE_PATH,
35
37
  URI_QUERIES,
36
38
  )
37
- from .exceptions import CannotAuthenticate, CannotRegisterDevice
39
+ from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
38
40
 
39
41
 
40
42
  @dataclass
@@ -58,7 +60,8 @@ class AmazonEchoApi:
58
60
  login_country_code: str,
59
61
  login_email: str,
60
62
  login_password: str,
61
- save_html: bool = False,
63
+ login_data_file: str | None,
64
+ save_raw_data: bool = False,
62
65
  ) -> None:
63
66
  """Initialize the scanner."""
64
67
  # Force country digits as lower case
@@ -79,24 +82,44 @@ class AmazonEchoApi:
79
82
  self._url = f"https://www.amazon.{domain}"
80
83
  self._cookies = self._build_init_cookies()
81
84
  self._headers = DEFAULT_HEADERS
82
- self._save_html = save_html
85
+ self._save_raw_data = save_raw_data
86
+ self._login_stored_data: dict[str, Any] = self._load_data_file(login_data_file)
83
87
  self._serial = self._serial_number()
88
+ self._website_cookies: dict[str, Any] = self._load_website_cookies()
84
89
 
85
90
  self.session: AsyncClient
86
91
 
92
+ def _load_data_file(self, data_file: str | None) -> dict[str, Any]:
93
+ """Load stored login data from file."""
94
+ if not data_file or not (file := Path(data_file)).exists():
95
+ _LOGGER.debug(
96
+ "Cannot find previous login data file <%s>",
97
+ data_file,
98
+ )
99
+ return {}
100
+
101
+ with Path.open(file, "rb") as f:
102
+ return cast(dict[str, Any], json.load(f))
103
+
104
+ def _load_website_cookies(self) -> dict[str, Any]:
105
+ """Get website cookies, if avaliables."""
106
+ if not self._login_stored_data:
107
+ return {}
108
+
109
+ return cast(dict, self._login_stored_data["website_cookies"])
110
+
87
111
  def _serial_number(self) -> str:
88
112
  """Get or calculate device serial number."""
89
- fullpath = Path(SAVE_PATH, "login_data.json")
90
- if not fullpath.exists():
113
+ if not self._login_stored_data:
91
114
  # Create a new serial number
92
115
  _LOGGER.debug("Cannot find previous login data, creating new serial number")
93
116
  return uuid.uuid4().hex.upper()
94
117
 
95
- with Path.open(fullpath, "rb") as file:
96
- data = json.load(file)
97
-
98
118
  _LOGGER.debug("Found previous login data, loading serial number")
99
- return cast(str, data["device_info"]["device_serial_number"])
119
+ return cast(
120
+ str,
121
+ self._login_stored_data["device_info"]["device_serial_number"],
122
+ )
100
123
 
101
124
  def _build_init_cookies(self) -> dict[str, str]:
102
125
  """Build initial cookies to prevent captcha in most cases."""
@@ -192,7 +215,7 @@ class AmazonEchoApi:
192
215
  parsed_url = parse_qs(url.query.decode())
193
216
  return parsed_url["openid.oa2.authorization_code"][0]
194
217
 
195
- def _client_session(self) -> None:
218
+ def _client_session(self, auth: Auth | None = None) -> None:
196
219
  """Create httpx ClientSession."""
197
220
  if not hasattr(self, "session") or self.session.is_closed:
198
221
  _LOGGER.debug("Creating HTTP ClientSession")
@@ -201,6 +224,7 @@ class AmazonEchoApi:
201
224
  headers=DEFAULT_HEADERS,
202
225
  cookies=self._cookies,
203
226
  follow_redirects=True,
227
+ auth=auth,
204
228
  )
205
229
 
206
230
  async def _session_request(
@@ -215,6 +239,7 @@ class AmazonEchoApi:
215
239
  method,
216
240
  url,
217
241
  data=input_data,
242
+ cookies=self._website_cookies,
218
243
  )
219
244
  content_type: str = resp.headers.get("Content-Type", "")
220
245
  _LOGGER.debug("Response content type: %s", content_type)
@@ -235,7 +260,7 @@ class AmazonEchoApi:
235
260
  output_path: str = SAVE_PATH,
236
261
  ) -> None:
237
262
  """Save response data to disk."""
238
- if not self._save_html:
263
+ if not self._save_raw_data:
239
264
  return
240
265
 
241
266
  output_dir = Path(output_path)
@@ -366,8 +391,8 @@ class AmazonEchoApi:
366
391
  await self._save_to_file(login_data, "login_data", JSON_EXTENSION)
367
392
  return login_data
368
393
 
369
- async def login(self, otp_code: str) -> dict[str, Any]:
370
- """Login to Amazon."""
394
+ async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
395
+ """Login to Amazon interactively via OTP."""
371
396
  _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
372
397
  self._client_session()
373
398
 
@@ -431,6 +456,28 @@ class AmazonEchoApi:
431
456
  _LOGGER.info("Register device: %s", register_device)
432
457
  return register_device
433
458
 
459
+ async def login_mode_stored_data(self) -> dict[str, Any]:
460
+ """Login to Amazon using previously stored data."""
461
+ if not self._login_stored_data:
462
+ _LOGGER.debug(
463
+ "Cannot find previous login data,\
464
+ use login_interactive() method instead",
465
+ )
466
+ raise WrongMethod
467
+
468
+ _LOGGER.debug(
469
+ "Logging-in for %s with stored data",
470
+ self._login_email,
471
+ )
472
+
473
+ auth = Authenticator.from_dict(
474
+ self._login_stored_data,
475
+ self._domain,
476
+ )
477
+ self._client_session(auth)
478
+
479
+ return self._login_stored_data
480
+
434
481
  async def close(self) -> None:
435
482
  """Close httpx session."""
436
483
  if hasattr(self, "session"):
@@ -441,7 +488,7 @@ class AmazonEchoApi:
441
488
  self,
442
489
  ) -> dict[str, Any]:
443
490
  """Get Amazon devices data."""
444
- devices = {}
491
+ devices: dict[str, Any] = {}
445
492
  for key in URI_QUERIES:
446
493
  _, raw_resp = await self._session_request(
447
494
  "GET",
@@ -457,10 +504,21 @@ class AmazonEchoApi:
457
504
 
458
505
  _LOGGER.debug("JSON data: |%s|", json_data)
459
506
 
460
- devices.update(
461
- {
462
- key: json_data,
463
- },
464
- )
465
-
466
- return devices
507
+ for data in json_data[key]:
508
+ dev_serial = data.get("serialNumber") or data.get("deviceSerialNumber")
509
+ if previous_data := devices.get(dev_serial):
510
+ devices[dev_serial] = previous_data | {key: data}
511
+ else:
512
+ devices[dev_serial] = {key: data}
513
+
514
+ # Remove stale, orphaned and virtual devices
515
+ final_devices_list: dict[str, Any] = devices.copy()
516
+ for serial in devices:
517
+ device = devices[serial]
518
+ if (
519
+ DEVICES not in device
520
+ or device[DEVICES].get("deviceType") == AMAZON_DEVICE_TYPE
521
+ ):
522
+ final_devices_list.pop(serial)
523
+
524
+ return final_devices_list
@@ -0,0 +1,315 @@
1
+ """Custom authentication module for httpx."""
2
+
3
+ import base64
4
+ from collections.abc import Generator
5
+ from datetime import UTC, datetime, timedelta
6
+ from typing import (
7
+ Any,
8
+ cast,
9
+ )
10
+
11
+ import httpx
12
+ from httpx import Cookies
13
+ from pyasn1.codec.der import decoder
14
+ from pyasn1.type import namedtype, univ
15
+ from rsa import PrivateKey, pkcs1
16
+
17
+ from .const import _LOGGER, AMAZON_APP_NAME, AMAZON_APP_VERSION
18
+ from .exceptions import (
19
+ AuthFlowError,
20
+ AuthMissingAccessToken,
21
+ AuthMissingRefreshToken,
22
+ AuthMissingSigningData,
23
+ AuthMissingTimestamp,
24
+ AuthMissingWebsiteCookies,
25
+ )
26
+
27
+
28
+ def base64_der_to_pkcs1(base64_key: str) -> str:
29
+ """Convert DER private key to PEM format."""
30
+
31
+ class PrivateKeyAlgorithm(univ.Sequence): # type: ignore[misc]
32
+ component_type = namedtype.NamedTypes(
33
+ namedtype.NamedType("algorithm", univ.ObjectIdentifier()),
34
+ namedtype.NamedType("parameters", univ.Any()),
35
+ )
36
+
37
+ class PrivateKeyInfo(univ.Sequence): # type: ignore[misc]
38
+ component_type = namedtype.NamedTypes(
39
+ namedtype.NamedType("version", univ.Integer()),
40
+ namedtype.NamedType("pkalgo", PrivateKeyAlgorithm()),
41
+ namedtype.NamedType("key", univ.OctetString()),
42
+ )
43
+
44
+ encoded_key = base64.b64decode(base64_key)
45
+ (key_info, _) = decoder.decode(encoded_key, asn1Spec=PrivateKeyInfo())
46
+ key_octet_string = key_info.components[2]
47
+ key = PrivateKey.load_pkcs1(key_octet_string, format="DER")
48
+ return cast(str, key.save_pkcs1().decode("utf-8"))
49
+
50
+
51
+ def refresh_access_token(
52
+ refresh_token: str,
53
+ domain: str,
54
+ ) -> dict[str, Any]:
55
+ """Refresh an access token."""
56
+ body = {
57
+ "app_name": AMAZON_APP_NAME,
58
+ "app_version": AMAZON_APP_VERSION,
59
+ "source_token": refresh_token,
60
+ "requested_token_type": "access_token",
61
+ "source_token_type": "refresh_token",
62
+ }
63
+
64
+ resp = httpx.post(f"https://api.amazon.{domain}/auth/token", data=body)
65
+ resp.raise_for_status()
66
+ resp_dict = resp.json()
67
+
68
+ expires_in_sec = int(resp_dict["expires_in"])
69
+ expires = (datetime.now(UTC) + timedelta(seconds=expires_in_sec)).timestamp()
70
+
71
+ return {"access_token": resp_dict["access_token"], "expires": expires}
72
+
73
+
74
+ def refresh_website_cookies(
75
+ refresh_token: str,
76
+ domain: str,
77
+ ) -> dict[str, str]:
78
+ """Fetch website cookies for a specific domain."""
79
+ url = f"https://www.amazon.{domain}/ap/exchangetoken/cookies"
80
+
81
+ body = {
82
+ "app_name": AMAZON_APP_NAME,
83
+ "app_version": AMAZON_APP_VERSION,
84
+ "source_token": refresh_token,
85
+ "requested_token_type": "auth_cookies",
86
+ "source_token_type": "refresh_token",
87
+ "domain": f".amazon.{domain}",
88
+ }
89
+
90
+ resp = httpx.post(url, data=body)
91
+ resp.raise_for_status()
92
+ resp_dict = resp.json()
93
+
94
+ raw_cookies = resp_dict["response"]["tokens"]["cookies"]
95
+ website_cookies = {}
96
+ for domain_cookies in raw_cookies:
97
+ for cookie in raw_cookies[domain_cookies]:
98
+ website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
99
+
100
+ return website_cookies
101
+
102
+
103
+ def user_profile(access_token: str, domain: str) -> dict[str, Any]:
104
+ """Return Amazon user profile from Amazon."""
105
+ headers = {"Authorization": f"Bearer {access_token}"}
106
+
107
+ resp = httpx.get(f"https://api.amazon.{domain}/user/profile", headers=headers)
108
+ resp.raise_for_status()
109
+ profile: dict[str, Any] = resp.json()
110
+
111
+ if "user_id" not in profile:
112
+ raise ValueError("Malformed user profile response.")
113
+
114
+ return profile
115
+
116
+
117
+ def sign_request(
118
+ method: str,
119
+ path: str,
120
+ body: bytes,
121
+ adp_token: str,
122
+ private_key: str,
123
+ ) -> dict[str, str]:
124
+ """Create signed headers for http requests."""
125
+ date = datetime.now(UTC).isoformat("T") + "Z"
126
+ str_body = body.decode("utf-8")
127
+
128
+ data = f"{method}\n{path}\n{date}\n{str_body}\n{adp_token}"
129
+
130
+ key = PrivateKey.load_pkcs1(private_key.encode("utf-8"))
131
+ cipher = pkcs1.sign(data.encode(), key, "SHA-256")
132
+ signed_encoded = base64.b64encode(cipher)
133
+
134
+ signature = f"{signed_encoded.decode()}:{date}"
135
+
136
+ return {
137
+ "x-adp-token": adp_token,
138
+ "x-adp-alg": "SHA256withRSA:1.0",
139
+ "x-adp-signature": signature,
140
+ }
141
+
142
+
143
+ class Authenticator(httpx.Auth): # type: ignore[misc]
144
+ """Amazon Authenticator class."""
145
+
146
+ access_token: str | None = None
147
+ activation_bytes: str | None = None
148
+ adp_token: str | None = None
149
+ customer_info: dict[str, Any] | None = None
150
+ device_info: dict[str, Any] | None = None
151
+ device_private_key: str | None = None
152
+ expires: float | None = None
153
+ domain: str
154
+ refresh_token: str | None = None
155
+ store_authentication_cookie: dict[str, Any] | None = None
156
+ website_cookies: dict[str, Any] | None = None
157
+ requires_request_body: bool = True
158
+ _forbid_new_attrs: bool = True
159
+ _apply_test_convert: bool = True
160
+
161
+ def update_attrs(self, **kwargs: Any) -> None: # noqa: ANN401
162
+ """Update attributes from dict."""
163
+ for attr, value in kwargs.items():
164
+ setattr(self, attr, value)
165
+
166
+ @classmethod
167
+ def from_dict(
168
+ cls,
169
+ data: dict[str, Any],
170
+ domain: str,
171
+ ) -> "Authenticator":
172
+ """Instantiate an Authenticator from authentication dictionary."""
173
+ auth = cls()
174
+
175
+ auth.domain = domain
176
+
177
+ if "login_cookies" in data:
178
+ auth.website_cookies = data.pop("login_cookies")
179
+
180
+ if data["device_private_key"].startswith("MII"):
181
+ pk_der = data["device_private_key"]
182
+ data["device_private_key"] = base64_der_to_pkcs1(pk_der)
183
+
184
+ auth.update_attrs(**data)
185
+
186
+ _LOGGER.info("load data from dictionary for domain %s", auth.domain)
187
+
188
+ return auth
189
+
190
+ def auth_flow(
191
+ self,
192
+ request: httpx.Request,
193
+ ) -> Generator[httpx.Request, httpx.Response, None]:
194
+ """Auth flow to be executed on every request by :mod:`httpx`."""
195
+ available_modes = self.available_auth_modes
196
+
197
+ _LOGGER.debug("Auth flow modes: %s", available_modes)
198
+ if "signing" in available_modes:
199
+ self._apply_signing_auth_flow(request)
200
+ elif "bearer" in available_modes:
201
+ self._apply_bearer_auth_flow(request)
202
+ else:
203
+ message = "signing or bearer auth flow are not available."
204
+ _LOGGER.critical(message)
205
+ raise AuthFlowError(message)
206
+
207
+ yield request
208
+
209
+ def _apply_signing_auth_flow(self, request: httpx.Request) -> None:
210
+ if self.adp_token is None or self.device_private_key is None:
211
+ raise AuthMissingSigningData
212
+
213
+ headers = sign_request(
214
+ method=request.method,
215
+ path=request.url.raw_path.decode(),
216
+ body=request.content,
217
+ adp_token=self.adp_token,
218
+ private_key=self.device_private_key,
219
+ )
220
+
221
+ request.headers.update(headers)
222
+ _LOGGER.info("signing auth flow applied to request")
223
+
224
+ def _apply_bearer_auth_flow(self, request: httpx.Request) -> None:
225
+ if self.access_token_expired:
226
+ self.refresh_access_token()
227
+
228
+ if self.access_token is None:
229
+ raise AuthMissingAccessToken
230
+
231
+ headers = {"Authorization": "Bearer " + self.access_token, "client-id": "0"}
232
+ request.headers.update(headers)
233
+ _LOGGER.info("bearer auth flow applied to request")
234
+
235
+ def _apply_cookies_auth_flow(self, request: httpx.Request) -> None:
236
+ if self.website_cookies is None:
237
+ raise AuthMissingWebsiteCookies
238
+ cookies = self.website_cookies.copy()
239
+
240
+ Cookies(cookies).set_cookie_header(request)
241
+ _LOGGER.info("cookies auth flow applied to request")
242
+
243
+ @property
244
+ def available_auth_modes(self) -> list[str]:
245
+ """List available authentication modes."""
246
+ available_modes = []
247
+
248
+ if self.adp_token and self.device_private_key:
249
+ available_modes.append("signing")
250
+
251
+ if self.access_token:
252
+ if self.access_token_expired and not self.refresh_token:
253
+ pass
254
+ else:
255
+ available_modes.append("bearer")
256
+
257
+ if self.website_cookies:
258
+ available_modes.append("cookies")
259
+
260
+ return available_modes
261
+
262
+ def refresh_access_token(self, force: bool = False) -> None:
263
+ """Refresh access token."""
264
+ if force or self.access_token_expired:
265
+ if self.refresh_token is None:
266
+ message = "No refresh token found. Can't refresh access token."
267
+ _LOGGER.critical(message)
268
+ raise AuthMissingRefreshToken(message)
269
+
270
+ refresh_data = refresh_access_token(
271
+ refresh_token=self.refresh_token,
272
+ domain=self.domain,
273
+ )
274
+
275
+ self.update_attrs(**refresh_data)
276
+ else:
277
+ _LOGGER.info(
278
+ "Access Token not expired. No refresh necessary. "
279
+ "To force refresh please use force=True",
280
+ )
281
+
282
+ def set_website_cookies_for_country(self) -> None:
283
+ """Set website cookies for country."""
284
+ if self.refresh_token is None:
285
+ raise AuthMissingRefreshToken
286
+
287
+ self.website_cookies = refresh_website_cookies(
288
+ self.refresh_token,
289
+ self.domain,
290
+ )
291
+
292
+ def user_profile(self) -> dict[str, Any]:
293
+ """Get user profile."""
294
+ if self.access_token is None:
295
+ raise AuthMissingAccessToken
296
+
297
+ return user_profile(access_token=self.access_token, domain=self.domain)
298
+
299
+ @property
300
+ def access_token_expires(self) -> timedelta:
301
+ """Time to access token expiration."""
302
+ if self.expires is None:
303
+ raise AuthMissingTimestamp
304
+ return datetime.fromtimestamp(self.expires, UTC) - datetime.now(
305
+ UTC,
306
+ )
307
+
308
+ @property
309
+ def access_token_expired(self) -> bool:
310
+ """Return True if access token is expired."""
311
+ if self.expires is None:
312
+ raise AuthMissingTimestamp
313
+ return datetime.fromtimestamp(self.expires, UTC) <= datetime.now(
314
+ UTC,
315
+ )
@@ -26,32 +26,32 @@ DOMAIN_BY_COUNTRY = {
26
26
  },
27
27
  }
28
28
 
29
+ # Amazon APP info
30
+ AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
31
+ AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
32
+ AMAZON_APP_NAME = "AioAmazonDevices"
33
+ AMAZON_APP_VERSION = "2.2.556530.0"
34
+ AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
35
+ AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
36
+ AMAZON_CLIENT_OS = "16.6"
37
+
29
38
  DEFAULT_HEADERS = {
30
39
  "User-Agent": (
31
- "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) "
40
+ f"Mozilla/5.0 (iPhone; CPU iPhone OS {AMAZON_CLIENT_OS.replace('.', '_')} like Mac OS X) " # noqa: E501
32
41
  "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
33
42
  ),
34
43
  "Accept-Language": "en-US",
35
44
  "Accept-Encoding": "gzip",
36
45
  }
37
46
 
47
+ DEVICES = "devices"
38
48
  URI_QUERIES = {
39
- "base": "/api/devices-v2/device",
40
- "status": "/api/dnd/device-status-list",
41
- "preferences": "/api/device-preferences",
42
- "automations": "/api/behaviors/v2/automations",
43
- "bluetooth": "/api/bluetooth",
49
+ DEVICES: "/api/devices-v2/device",
50
+ "doNotDisturbDeviceStatusList": "/api/dnd/device-status-list",
51
+ "devicePreferences": "/api/device-preferences",
52
+ "bluetoothStates": "/api/bluetooth",
44
53
  }
45
54
 
46
- # Amazon APP info
47
- AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
48
- AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
49
- AMAZON_APP_NAME = "AioAmazonDevices"
50
- AMAZON_APP_VERSION = "2.2.556530.0"
51
- AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
52
- AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
53
- AMAZON_CLIENT_OS = "16.6"
54
-
55
55
  # File extensions
56
56
  SAVE_PATH = "out"
57
57
  HTML_EXTENSION = ".html"
@@ -0,0 +1,51 @@
1
+ """Comelit SimpleHome library exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AmazonError(Exception):
7
+ """Base class for aioamazondevices errors."""
8
+
9
+
10
+ class CannotConnect(AmazonError):
11
+ """Exception raised when connection fails."""
12
+
13
+
14
+ class CannotAuthenticate(AmazonError):
15
+ """Exception raised when credentials are incorrect."""
16
+
17
+
18
+ class CannotRetrieveData(AmazonError):
19
+ """Exception raised when data retrieval fails."""
20
+
21
+
22
+ class CannotRegisterDevice(AmazonError):
23
+ """Exception raised when device registration fails."""
24
+
25
+
26
+ class WrongMethod(AmazonError):
27
+ """Exception raised when the wrong login metho is used."""
28
+
29
+
30
+ class AuthFlowError(AmazonError):
31
+ """Exception raised when auth flow fails."""
32
+
33
+
34
+ class AuthMissingTimestamp(AmazonError):
35
+ """Exception raised when expires timestamp is missing."""
36
+
37
+
38
+ class AuthMissingAccessToken(AmazonError):
39
+ """Exception raised when access token is missing."""
40
+
41
+
42
+ class AuthMissingRefreshToken(AmazonError):
43
+ """Exception raised when refresh token is missing."""
44
+
45
+
46
+ class AuthMissingSigningData(AmazonError):
47
+ """Exception raised when some data for signing are missing."""
48
+
49
+
50
+ class AuthMissingWebsiteCookies(AmazonError):
51
+ """Exception raised when website cookies are missing."""
@@ -1,23 +0,0 @@
1
- """Comelit SimpleHome library exceptions."""
2
-
3
- from __future__ import annotations
4
-
5
-
6
- class AmazonError(Exception):
7
- """Base class for aioamazondevices errors."""
8
-
9
-
10
- class CannotConnect(AmazonError):
11
- """Exception raised when connection fails."""
12
-
13
-
14
- class CannotAuthenticate(AmazonError):
15
- """Exception raised when credentials are incorrect."""
16
-
17
-
18
- class CannotRetrieveData(AmazonError):
19
- """Exception raised when data retrieval fails."""
20
-
21
-
22
- class CannotRegisterDevice(AmazonError):
23
- """Exception raised when device registration fails."""