aioamazondevices 8.0.1__tar.gz → 10.0.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.
Files changed (23) hide show
  1. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/PKG-INFO +10 -3
  2. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/README.md +9 -2
  3. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/pyproject.toml +3 -3
  4. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/__init__.py +1 -1
  5. aioamazondevices-10.0.0/src/aioamazondevices/api.py +540 -0
  6. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/devices.py +45 -0
  7. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/metadata.py +2 -0
  8. aioamazondevices-10.0.0/src/aioamazondevices/http_wrapper.py +360 -0
  9. aioamazondevices-10.0.0/src/aioamazondevices/implementation/__init__.py +1 -0
  10. aioamazondevices-10.0.0/src/aioamazondevices/implementation/notification.py +223 -0
  11. aioamazondevices-10.0.0/src/aioamazondevices/implementation/sequence.py +159 -0
  12. aioamazondevices-10.0.0/src/aioamazondevices/login.py +495 -0
  13. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/structures.py +1 -1
  14. aioamazondevices-8.0.1/src/aioamazondevices/api.py +0 -1489
  15. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/LICENSE +0 -0
  16. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/__init__.py +0 -0
  17. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/http.py +0 -0
  18. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/queries.py +0 -0
  19. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/schedules.py +0 -0
  20. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/const/sounds.py +0 -0
  21. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/exceptions.py +0 -0
  22. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/py.typed +0 -0
  23. {aioamazondevices-8.0.1 → aioamazondevices-10.0.0}/src/aioamazondevices/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioamazondevices
3
- Version: 8.0.1
3
+ Version: 10.0.0
4
4
  Summary: Python library to control Amazon devices
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -134,6 +134,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
134
134
  <sub><b>Ivan F. Martinez</b></sub>
135
135
  </a>
136
136
  </td>
137
+ <td align="center">
138
+ <a href="https://github.com/eyadkobatte">
139
+ <img src="https://avatars.githubusercontent.com/u/16541074?v=4" width="100;" alt="eyadkobatte"/>
140
+ <br />
141
+ <sub><b>Eyad Kobatte</b></sub>
142
+ </a>
143
+ </td>
137
144
  <td align="center">
138
145
  <a href="https://github.com/AzonInc">
139
146
  <img src="https://avatars.githubusercontent.com/u/11911587?v=4" width="100;" alt="AzonInc"/>
@@ -141,6 +148,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
141
148
  <sub><b>Flo</b></sub>
142
149
  </a>
143
150
  </td>
151
+ </tr>
152
+ <tr>
144
153
  <td align="center">
145
154
  <a href="https://github.com/francescolf">
146
155
  <img src="https://avatars.githubusercontent.com/u/14892143?v=4" width="100;" alt="francescolf"/>
@@ -148,8 +157,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
148
157
  <sub><b>Francesco Lo Faro</b></sub>
149
158
  </a>
150
159
  </td>
151
- </tr>
152
- <tr>
153
160
  <td align="center">
154
161
  <a href="https://github.com/lchavezcuu">
155
162
  <img src="https://avatars.githubusercontent.com/u/22165856?v=4" width="100;" alt="lchavezcuu"/>
@@ -109,6 +109,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
109
109
  <sub><b>Ivan F. Martinez</b></sub>
110
110
  </a>
111
111
  </td>
112
+ <td align="center">
113
+ <a href="https://github.com/eyadkobatte">
114
+ <img src="https://avatars.githubusercontent.com/u/16541074?v=4" width="100;" alt="eyadkobatte"/>
115
+ <br />
116
+ <sub><b>Eyad Kobatte</b></sub>
117
+ </a>
118
+ </td>
112
119
  <td align="center">
113
120
  <a href="https://github.com/AzonInc">
114
121
  <img src="https://avatars.githubusercontent.com/u/11911587?v=4" width="100;" alt="AzonInc"/>
@@ -116,6 +123,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
116
123
  <sub><b>Flo</b></sub>
117
124
  </a>
118
125
  </td>
126
+ </tr>
127
+ <tr>
119
128
  <td align="center">
120
129
  <a href="https://github.com/francescolf">
121
130
  <img src="https://avatars.githubusercontent.com/u/14892143?v=4" width="100;" alt="francescolf"/>
@@ -123,8 +132,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
123
132
  <sub><b>Francesco Lo Faro</b></sub>
124
133
  </a>
125
134
  </td>
126
- </tr>
127
- <tr>
128
135
  <td align="center">
129
136
  <a href="https://github.com/lchavezcuu">
130
137
  <img src="https://avatars.githubusercontent.com/u/22165856?v=4" width="100;" alt="lchavezcuu"/>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "8.0.1"
3
+ version = "10.0.0"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -34,9 +34,9 @@ dependencies = [
34
34
  "Changelog" = "https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md"
35
35
 
36
36
  [tool.poetry.group.dev.dependencies]
37
- pytest = "^8.4"
37
+ pytest = "^9.0"
38
38
  pytest-cov = ">=5,<8"
39
- types-python-dateutil = "^2.9.0.20250822"
39
+ types-python-dateutil = "^2.9.0.20251115"
40
40
 
41
41
  [tool.semantic_release]
42
42
  version_toml = ["pyproject.toml:project.version"]
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "8.0.1"
3
+ __version__ = "10.0.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -0,0 +1,540 @@
1
+ """Support for Amazon devices."""
2
+
3
+ from collections.abc import Callable, Coroutine
4
+ from datetime import UTC, datetime, timedelta
5
+ from http import HTTPMethod
6
+ from typing import Any
7
+
8
+ from aiohttp import ClientSession
9
+
10
+ from . import __version__
11
+ from .const.devices import (
12
+ DEVICE_TO_IGNORE,
13
+ DEVICE_TYPE_TO_MODEL,
14
+ SPEAKER_GROUP_FAMILY,
15
+ )
16
+ from .const.http import (
17
+ ARRAY_WRAPPER,
18
+ DEFAULT_SITE,
19
+ URI_DEVICES,
20
+ URI_DND,
21
+ URI_NEXUS_GRAPHQL,
22
+ )
23
+ from .const.metadata import SENSORS
24
+ from .const.queries import QUERY_DEVICE_DATA, QUERY_SENSOR_STATE
25
+ from .const.schedules import (
26
+ NOTIFICATION_ALARM,
27
+ NOTIFICATION_REMINDER,
28
+ NOTIFICATION_TIMER,
29
+ )
30
+ from .exceptions import (
31
+ CannotRetrieveData,
32
+ )
33
+ from .http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
34
+ from .implementation.notification import AmazonNotificationHandler
35
+ from .implementation.sequence import AmazonSequenceHandler
36
+ from .login import AmazonLogin
37
+ from .structures import (
38
+ AmazonDevice,
39
+ AmazonDeviceSensor,
40
+ AmazonMusicSource,
41
+ AmazonSequenceType,
42
+ )
43
+ from .utils import _LOGGER
44
+
45
+
46
+ class AmazonEchoApi:
47
+ """Queries Amazon for Echo devices."""
48
+
49
+ def __init__(
50
+ self,
51
+ client_session: ClientSession,
52
+ login_email: str,
53
+ login_password: str,
54
+ login_data: dict[str, Any] | None = None,
55
+ save_to_file: Callable[[str | dict, str, str], Coroutine[Any, Any, None]]
56
+ | None = None,
57
+ ) -> None:
58
+ """Initialize the scanner."""
59
+ _LOGGER.debug("Initialize library v%s", __version__)
60
+
61
+ # Check if there is a previous login, otherwise use default (US)
62
+ site = login_data.get("site", DEFAULT_SITE) if login_data else DEFAULT_SITE
63
+ _LOGGER.debug("Using site: %s", site)
64
+
65
+ self._session_state_data = AmazonSessionStateData(
66
+ site, login_email, login_password, login_data
67
+ )
68
+
69
+ self._http_wrapper = AmazonHttpWrapper(
70
+ client_session,
71
+ self._session_state_data,
72
+ save_to_file,
73
+ )
74
+
75
+ self._login = AmazonLogin(
76
+ http_wrapper=self._http_wrapper,
77
+ session_state_data=self._session_state_data,
78
+ )
79
+
80
+ self._notification_handler = AmazonNotificationHandler(
81
+ http_wrapper=self._http_wrapper,
82
+ session_state_data=self._session_state_data,
83
+ )
84
+
85
+ self._sequence_handler = AmazonSequenceHandler(
86
+ http_wrapper=self._http_wrapper,
87
+ session_state_data=self._session_state_data,
88
+ )
89
+
90
+ self._final_devices: dict[str, AmazonDevice] = {}
91
+ self._endpoints: dict[str, str] = {} # endpoint ID to serial number map
92
+
93
+ initial_time = datetime.now(UTC) - timedelta(days=2) # force initial refresh
94
+ self._last_devices_refresh: datetime = initial_time
95
+ self._last_endpoint_refresh: datetime = initial_time
96
+
97
+ @property
98
+ def domain(self) -> str:
99
+ """Return current Amazon domain."""
100
+ return self._session_state_data.domain
101
+
102
+ @property
103
+ def login(self) -> AmazonLogin:
104
+ """Return login."""
105
+ return self._login
106
+
107
+ async def _get_sensors_states(self) -> dict[str, dict[str, AmazonDeviceSensor]]:
108
+ """Retrieve devices sensors states."""
109
+ devices_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
110
+
111
+ if not self._endpoints:
112
+ return {}
113
+
114
+ endpoint_ids = list(self._endpoints.keys())
115
+ payload = [
116
+ {
117
+ "operationName": "getEndpointState",
118
+ "variables": {
119
+ "endpointIds": endpoint_ids,
120
+ },
121
+ "query": QUERY_SENSOR_STATE,
122
+ }
123
+ ]
124
+
125
+ _, raw_resp = await self._http_wrapper.session_request(
126
+ method=HTTPMethod.POST,
127
+ url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
128
+ input_data=payload,
129
+ json_data=True,
130
+ )
131
+
132
+ sensors_state = await self._http_wrapper.response_to_json(raw_resp, "sensors")
133
+
134
+ if await self._format_human_error(sensors_state):
135
+ # Explicit error in returned data
136
+ return {}
137
+
138
+ if (
139
+ not (arr := sensors_state.get(ARRAY_WRAPPER))
140
+ or not (data := arr[0].get("data"))
141
+ or not (endpoints_list := data.get("listEndpoints"))
142
+ or not (endpoints := endpoints_list.get("endpoints"))
143
+ ):
144
+ _LOGGER.error("Malformed sensor state data received: %s", sensors_state)
145
+ return {}
146
+
147
+ for endpoint in endpoints:
148
+ serial_number = self._endpoints[endpoint.get("endpointId")]
149
+
150
+ if serial_number in self._final_devices:
151
+ devices_sensors[serial_number] = self._get_device_sensor_state(
152
+ endpoint, serial_number
153
+ )
154
+
155
+ return devices_sensors
156
+
157
+ def _get_device_sensor_state(
158
+ self, endpoint: dict[str, Any], serial_number: str
159
+ ) -> dict[str, AmazonDeviceSensor]:
160
+ device_sensors: dict[str, AmazonDeviceSensor] = {}
161
+ for feature in endpoint.get("features", {}):
162
+ if (sensor_template := SENSORS.get(feature["name"])) is None:
163
+ # Skip sensors that are not in the predefined list
164
+ continue
165
+
166
+ if not (name := sensor_template["name"]):
167
+ raise CannotRetrieveData("Unable to read sensor template")
168
+
169
+ for feature_property in feature.get("properties"):
170
+ if sensor_template["name"] != feature_property.get("name"):
171
+ continue
172
+
173
+ value: str | int | float = "n/a"
174
+ scale: str | None = None
175
+
176
+ # "error" can be None, missing, or a dict
177
+ api_error = feature_property.get("error") or {}
178
+ error = bool(api_error)
179
+ error_type = api_error.get("type")
180
+ error_msg = api_error.get("message")
181
+ if not error:
182
+ try:
183
+ value_raw = feature_property[sensor_template["key"]]
184
+ if not value_raw:
185
+ _LOGGER.warning(
186
+ "Sensor %s [device %s] ignored due to empty value",
187
+ name,
188
+ serial_number,
189
+ )
190
+ continue
191
+ scale = (
192
+ value_raw[scale_template]
193
+ if (scale_template := sensor_template["scale"])
194
+ else None
195
+ )
196
+ value = (
197
+ value_raw[subkey_template]
198
+ if (subkey_template := sensor_template["subkey"])
199
+ else value_raw
200
+ )
201
+
202
+ except (KeyError, ValueError) as exc:
203
+ _LOGGER.warning(
204
+ "Sensor %s [device %s] ignored due to errors in feature %s: %s", # noqa: E501
205
+ name,
206
+ serial_number,
207
+ feature_property,
208
+ repr(exc),
209
+ )
210
+ if error:
211
+ _LOGGER.debug(
212
+ "error in sensor %s - %s - %s", name, error_type, error_msg
213
+ )
214
+
215
+ if error_type != "NOT_FOUND":
216
+ device_sensors[name] = AmazonDeviceSensor(
217
+ name,
218
+ value,
219
+ error,
220
+ error_type,
221
+ error_msg,
222
+ scale,
223
+ )
224
+
225
+ return device_sensors
226
+
227
+ async def _get_devices_endpoint_data(self) -> dict[str, dict[str, Any]]:
228
+ """Get Devices endpoint data."""
229
+ payload = {
230
+ "operationName": "getDevicesBaseData",
231
+ "query": QUERY_DEVICE_DATA,
232
+ }
233
+
234
+ _, raw_resp = await self._http_wrapper.session_request(
235
+ method=HTTPMethod.POST,
236
+ url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
237
+ input_data=payload,
238
+ json_data=True,
239
+ )
240
+
241
+ endpoint_data = await self._http_wrapper.response_to_json(raw_resp, "endpoint")
242
+
243
+ if not (data := endpoint_data.get("data")) or not data.get("listEndpoints"):
244
+ await self._format_human_error(endpoint_data)
245
+ return {}
246
+
247
+ endpoints = data["listEndpoints"]
248
+ devices_endpoints: dict[str, dict[str, Any]] = {}
249
+ for endpoint in endpoints.get("endpoints"):
250
+ # save looking up sensor data on apps
251
+ if endpoint.get("alexaEnabledMetadata", {}).get("category") == "APP":
252
+ continue
253
+
254
+ if endpoint.get("serialNumber"):
255
+ serial_number = endpoint["serialNumber"]["value"]["text"]
256
+ devices_endpoints[serial_number] = endpoint
257
+ self._endpoints[endpoint["endpointId"]] = serial_number
258
+
259
+ return devices_endpoints
260
+
261
+ async def get_devices_data(
262
+ self,
263
+ ) -> dict[str, AmazonDevice]:
264
+ """Get Amazon devices data."""
265
+ delta_devices = datetime.now(UTC) - self._last_devices_refresh
266
+ if delta_devices >= timedelta(days=1):
267
+ _LOGGER.debug(
268
+ "Refreshing devices data after %s",
269
+ str(timedelta(minutes=round(delta_devices.total_seconds() / 60))),
270
+ )
271
+ # Request base device data
272
+ await self._get_base_devices()
273
+ self._last_devices_refresh = datetime.now(UTC)
274
+
275
+ # Only refresh endpoint data if we have no endpoints yet
276
+ delta_endpoints = datetime.now(UTC) - self._last_endpoint_refresh
277
+ endpoint_refresh_needed = delta_endpoints >= timedelta(days=1)
278
+ endpoints_recently_checked = delta_endpoints < timedelta(minutes=30)
279
+ if (
280
+ not self._endpoints and not endpoints_recently_checked
281
+ ) or endpoint_refresh_needed:
282
+ _LOGGER.debug(
283
+ "Refreshing endpoint data after %s",
284
+ str(timedelta(minutes=round(delta_endpoints.total_seconds() / 60))),
285
+ )
286
+ # Set device endpoint data
287
+ await self._set_device_endpoints_data()
288
+ self._last_endpoint_refresh = datetime.now(UTC)
289
+
290
+ await self._get_sensor_data()
291
+
292
+ return self._final_devices
293
+
294
+ async def _get_sensor_data(self) -> None:
295
+ devices_sensors = await self._get_sensors_states()
296
+ dnd_sensors = await self._get_dnd_status()
297
+ notifications = await self._notification_handler.get_notifications()
298
+ for device in self._final_devices.values():
299
+ # Update sensors
300
+ sensors = devices_sensors.get(device.serial_number, {})
301
+ if sensors:
302
+ device.sensors = sensors
303
+ else:
304
+ for device_sensor in device.sensors.values():
305
+ device_sensor.error = True
306
+ if (
307
+ device_dnd := dnd_sensors.get(device.serial_number)
308
+ ) and device.device_family != SPEAKER_GROUP_FAMILY:
309
+ device.sensors["dnd"] = device_dnd
310
+
311
+ if notifications is None:
312
+ continue # notifications were not obtained, do not update
313
+
314
+ # Clear old notifications to handle cancelled ones
315
+ device.notifications = {}
316
+
317
+ # Update notifications
318
+ device_notifications = notifications.get(device.serial_number, {})
319
+
320
+ # Add only supported notification types
321
+ for capability, notification_type in [
322
+ ("REMINDERS", NOTIFICATION_REMINDER),
323
+ ("TIMERS_AND_ALARMS", NOTIFICATION_ALARM),
324
+ ("TIMERS_AND_ALARMS", NOTIFICATION_TIMER),
325
+ ]:
326
+ if (
327
+ capability in device.capabilities
328
+ and notification_type in device_notifications
329
+ and (
330
+ notification_object := device_notifications.get(
331
+ notification_type
332
+ )
333
+ )
334
+ ):
335
+ device.notifications[notification_type] = notification_object
336
+
337
+ async def _set_device_endpoints_data(self) -> None:
338
+ """Set device endpoint data."""
339
+ devices_endpoints = await self._get_devices_endpoint_data()
340
+ for serial_number in self._final_devices:
341
+ device_endpoint = devices_endpoints.get(serial_number, {})
342
+ endpoint_device = self._final_devices[serial_number]
343
+ endpoint_device.entity_id = (
344
+ device_endpoint["legacyIdentifiers"]["chrsIdentifier"]["entityId"]
345
+ if device_endpoint
346
+ else None
347
+ )
348
+ endpoint_device.endpoint_id = (
349
+ device_endpoint["endpointId"] if device_endpoint else None
350
+ )
351
+
352
+ async def _get_base_devices(self) -> None:
353
+ _, raw_resp = await self._http_wrapper.session_request(
354
+ method=HTTPMethod.GET,
355
+ url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_DEVICES}",
356
+ )
357
+
358
+ json_data = await self._http_wrapper.response_to_json(raw_resp, "devices")
359
+
360
+ final_devices_list: dict[str, AmazonDevice] = {}
361
+ serial_to_device_type: dict[str, str] = {}
362
+ for device in json_data["devices"]:
363
+ # Remove stale, orphaned and virtual devices
364
+ if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
365
+ continue
366
+
367
+ account_name: str = device["accountName"]
368
+ capabilities: list[str] = device["capabilities"]
369
+ # Skip devices that cannot be used with voice features
370
+ if "MICROPHONE" not in capabilities:
371
+ _LOGGER.debug(
372
+ "Skipping device without microphone capabilities: %s", account_name
373
+ )
374
+ continue
375
+
376
+ serial_number: str = device["serialNumber"]
377
+
378
+ final_devices_list[serial_number] = AmazonDevice(
379
+ account_name=account_name,
380
+ capabilities=capabilities,
381
+ device_family=device["deviceFamily"],
382
+ device_type=device["deviceType"],
383
+ device_owner_customer_id=device["deviceOwnerCustomerId"],
384
+ household_device=device["deviceOwnerCustomerId"]
385
+ == self._session_state_data.account_customer_id,
386
+ device_cluster_members=dict.fromkeys(
387
+ device["clusterMembers"] or [serial_number]
388
+ ),
389
+ online=device["online"],
390
+ serial_number=serial_number,
391
+ software_version=device["softwareVersion"],
392
+ entity_id=None,
393
+ endpoint_id=None,
394
+ sensors={},
395
+ notifications={},
396
+ )
397
+
398
+ serial_to_device_type[serial_number] = device["deviceType"]
399
+
400
+ # backfill device types for cluster members
401
+ for device in final_devices_list.values():
402
+ for member_serial in device.device_cluster_members:
403
+ device.device_cluster_members[member_serial] = (
404
+ serial_to_device_type.get(member_serial)
405
+ )
406
+
407
+ self._final_devices = final_devices_list
408
+
409
+ def get_model_details(self, device: AmazonDevice) -> dict[str, str | None] | None:
410
+ """Return model datails."""
411
+ model_details: dict[str, str | None] | None = DEVICE_TYPE_TO_MODEL.get(
412
+ device.device_type
413
+ )
414
+ if not model_details:
415
+ _LOGGER.warning(
416
+ "Unknown device type '%s' for %s: please read https://github.com/chemelli74/aioamazondevices/wiki/Unknown-Device-Types",
417
+ device.device_type,
418
+ device.account_name,
419
+ )
420
+
421
+ return model_details
422
+
423
+ async def call_alexa_speak(
424
+ self,
425
+ device: AmazonDevice,
426
+ text_to_speak: str,
427
+ ) -> None:
428
+ """Call Alexa.Speak to send a message."""
429
+ await self._sequence_handler.send_message(
430
+ device, AmazonSequenceType.Speak, text_to_speak
431
+ )
432
+
433
+ async def call_alexa_announcement(
434
+ self,
435
+ device: AmazonDevice,
436
+ text_to_announce: str,
437
+ ) -> None:
438
+ """Call AlexaAnnouncement to send a message."""
439
+ await self._sequence_handler.send_message(
440
+ device, AmazonSequenceType.Announcement, text_to_announce
441
+ )
442
+
443
+ async def call_alexa_sound(
444
+ self,
445
+ device: AmazonDevice,
446
+ sound_name: str,
447
+ ) -> None:
448
+ """Call Alexa.Sound to play sound."""
449
+ await self._sequence_handler.send_message(
450
+ device, AmazonSequenceType.Sound, sound_name
451
+ )
452
+
453
+ async def call_alexa_music(
454
+ self,
455
+ device: AmazonDevice,
456
+ search_phrase: str,
457
+ music_source: AmazonMusicSource,
458
+ ) -> None:
459
+ """Call Alexa.Music.PlaySearchPhrase to play music."""
460
+ await self._sequence_handler.send_message(
461
+ device, AmazonSequenceType.Music, search_phrase, music_source
462
+ )
463
+
464
+ async def call_alexa_text_command(
465
+ self,
466
+ device: AmazonDevice,
467
+ text_command: str,
468
+ ) -> None:
469
+ """Call Alexa.TextCommand to issue command."""
470
+ await self._sequence_handler.send_message(
471
+ device, AmazonSequenceType.TextCommand, text_command
472
+ )
473
+
474
+ async def call_alexa_skill(
475
+ self,
476
+ device: AmazonDevice,
477
+ skill_name: str,
478
+ ) -> None:
479
+ """Call Alexa.LaunchSkill to launch a skill."""
480
+ await self._sequence_handler.send_message(
481
+ device, AmazonSequenceType.LaunchSkill, skill_name
482
+ )
483
+
484
+ async def call_alexa_info_skill(
485
+ self,
486
+ device: AmazonDevice,
487
+ info_skill_name: str,
488
+ ) -> None:
489
+ """Call Info skill. See ALEXA_INFO_SKILLS . const."""
490
+ await self._sequence_handler.send_message(device, info_skill_name, "")
491
+
492
+ async def set_do_not_disturb(self, device: AmazonDevice, state: bool) -> None:
493
+ """Set do_not_disturb flag."""
494
+ payload = {
495
+ "deviceSerialNumber": device.serial_number,
496
+ "deviceType": device.device_type,
497
+ "enabled": state,
498
+ }
499
+ url = f"https://alexa.amazon.{self._session_state_data.domain}/api/dnd/status"
500
+ await self._http_wrapper.session_request(
501
+ method="PUT",
502
+ url=url,
503
+ input_data=payload,
504
+ json_data=True,
505
+ )
506
+
507
+ async def _get_dnd_status(self) -> dict[str, AmazonDeviceSensor]:
508
+ dnd_status: dict[str, AmazonDeviceSensor] = {}
509
+ _, raw_resp = await self._http_wrapper.session_request(
510
+ method=HTTPMethod.GET,
511
+ url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_DND}",
512
+ )
513
+
514
+ dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
515
+
516
+ for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
517
+ dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
518
+ name="dnd",
519
+ value=dnd.get("enabled"),
520
+ error=False,
521
+ error_type=None,
522
+ error_msg=None,
523
+ scale=None,
524
+ )
525
+ return dnd_status
526
+
527
+ async def _format_human_error(self, sensors_state: dict) -> bool:
528
+ """Format human readable error from malformed data."""
529
+ if sensors_state.get(ARRAY_WRAPPER):
530
+ error = sensors_state[ARRAY_WRAPPER][0].get("errors", [])
531
+ else:
532
+ error = sensors_state.get("errors", [])
533
+
534
+ if not error:
535
+ return False
536
+
537
+ msg = error[0].get("message", "Unknown error")
538
+ path = error[0].get("path", "Unknown path")
539
+ _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
540
+ return True