denonavr 0.11.2__py3-none-any.whl → 0.11.6__py3-none-any.whl
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.
- denonavr/__init__.py +1 -1
- denonavr/api.py +194 -59
- denonavr/audyssey.py +69 -32
- denonavr/const.py +201 -12
- denonavr/decorators.py +27 -41
- denonavr/denonavr.py +50 -7
- denonavr/foundation.py +87 -54
- denonavr/input.py +119 -72
- denonavr/soundmode.py +32 -16
- denonavr/ssdp.py +13 -13
- denonavr/tonecontrol.py +91 -36
- denonavr/volume.py +53 -17
- {denonavr-0.11.2.dist-info → denonavr-0.11.6.dist-info}/METADATA +13 -11
- denonavr-0.11.6.dist-info/RECORD +19 -0
- {denonavr-0.11.2.dist-info → denonavr-0.11.6.dist-info}/WHEEL +1 -1
- denonavr-0.11.2.dist-info/RECORD +0 -19
- {denonavr-0.11.2.dist-info → denonavr-0.11.6.dist-info}/LICENSE +0 -0
- {denonavr-0.11.2.dist-info → denonavr-0.11.6.dist-info}/top_level.txt +0 -0
denonavr/__init__.py
CHANGED
denonavr/api.py
CHANGED
|
@@ -13,16 +13,31 @@ import logging
|
|
|
13
13
|
import sys
|
|
14
14
|
import time
|
|
15
15
|
import xml.etree.ElementTree as ET
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from collections.abc import Hashable
|
|
16
18
|
from io import BytesIO
|
|
17
|
-
from typing import
|
|
19
|
+
from typing import (
|
|
20
|
+
Awaitable,
|
|
21
|
+
Callable,
|
|
22
|
+
Coroutine,
|
|
23
|
+
DefaultDict,
|
|
24
|
+
Dict,
|
|
25
|
+
List,
|
|
26
|
+
Optional,
|
|
27
|
+
Set,
|
|
28
|
+
Tuple,
|
|
29
|
+
cast,
|
|
30
|
+
)
|
|
18
31
|
|
|
19
32
|
import attr
|
|
20
33
|
import httpx
|
|
21
|
-
from asyncstdlib import lru_cache
|
|
22
34
|
from defusedxml.ElementTree import fromstring
|
|
23
35
|
|
|
24
36
|
from .appcommand import AppCommandCmd
|
|
25
37
|
from .const import (
|
|
38
|
+
ALL_TELNET_EVENTS,
|
|
39
|
+
ALL_ZONE_TELNET_EVENTS,
|
|
40
|
+
ALL_ZONES,
|
|
26
41
|
APPCOMMAND0300_URL,
|
|
27
42
|
APPCOMMAND_CMD_TEXT,
|
|
28
43
|
APPCOMMAND_NAME,
|
|
@@ -34,15 +49,12 @@ from .const import (
|
|
|
34
49
|
ZONE2,
|
|
35
50
|
ZONE3,
|
|
36
51
|
)
|
|
37
|
-
from .decorators import
|
|
38
|
-
async_handle_receiver_exceptions,
|
|
39
|
-
cache_clear_on_exception,
|
|
40
|
-
set_cache_id,
|
|
41
|
-
)
|
|
52
|
+
from .decorators import async_handle_receiver_exceptions, cache_result
|
|
42
53
|
from .exceptions import (
|
|
43
54
|
AvrIncompleteResponseError,
|
|
44
55
|
AvrInvalidResponseError,
|
|
45
56
|
AvrNetworkError,
|
|
57
|
+
AvrProcessingError,
|
|
46
58
|
AvrTimoutError,
|
|
47
59
|
)
|
|
48
60
|
|
|
@@ -61,6 +73,16 @@ def get_default_async_client() -> httpx.AsyncClient:
|
|
|
61
73
|
return httpx.AsyncClient()
|
|
62
74
|
|
|
63
75
|
|
|
76
|
+
def telnet_event_map_factory() -> Dict[str, List]:
|
|
77
|
+
"""Create telnet event map."""
|
|
78
|
+
event_map: DefaultDict[str, List] = defaultdict(list)
|
|
79
|
+
for event in TELNET_EVENTS:
|
|
80
|
+
event_map[event[0:2]].append(event)
|
|
81
|
+
for value in event_map.values():
|
|
82
|
+
value.sort(key=len, reverse=True)
|
|
83
|
+
return dict(event_map)
|
|
84
|
+
|
|
85
|
+
|
|
64
86
|
@attr.s(auto_attribs=True, hash=False, on_setattr=DENON_ATTR_SETATTR)
|
|
65
87
|
class DenonAVRApi:
|
|
66
88
|
"""Perform API calls to Denon AVR REST interface."""
|
|
@@ -107,9 +129,7 @@ class DenonAVRApi:
|
|
|
107
129
|
# Use default port of the receiver if no different port is specified
|
|
108
130
|
port = port if port is not None else self.port
|
|
109
131
|
|
|
110
|
-
endpoint = "http://{host}:{port}{request}"
|
|
111
|
-
host=self.host, port=port, request=request
|
|
112
|
-
)
|
|
132
|
+
endpoint = f"http://{self.host}:{port}{request}"
|
|
113
133
|
|
|
114
134
|
client = self.async_client_getter()
|
|
115
135
|
try:
|
|
@@ -134,9 +154,7 @@ class DenonAVRApi:
|
|
|
134
154
|
# Use default port of the receiver if no different port is specified
|
|
135
155
|
port = port if port is not None else self.port
|
|
136
156
|
|
|
137
|
-
endpoint = "http://{host}:{port}{request}"
|
|
138
|
-
host=self.host, port=port, request=request
|
|
139
|
-
)
|
|
157
|
+
endpoint = f"http://{self.host}:{port}{request}"
|
|
140
158
|
|
|
141
159
|
client = self.async_client_getter()
|
|
142
160
|
try:
|
|
@@ -159,9 +177,7 @@ class DenonAVRApi:
|
|
|
159
177
|
# Return text
|
|
160
178
|
return res.text
|
|
161
179
|
|
|
162
|
-
@
|
|
163
|
-
@cache_clear_on_exception
|
|
164
|
-
@lru_cache(maxsize=32)
|
|
180
|
+
@cache_result
|
|
165
181
|
@async_handle_receiver_exceptions
|
|
166
182
|
async def async_get_xml(
|
|
167
183
|
self, request: str, cache_id: Hashable = None
|
|
@@ -176,9 +192,7 @@ class DenonAVRApi:
|
|
|
176
192
|
# Return ElementTree element
|
|
177
193
|
return xml_root
|
|
178
194
|
|
|
179
|
-
@
|
|
180
|
-
@cache_clear_on_exception
|
|
181
|
-
@lru_cache(maxsize=32)
|
|
195
|
+
@cache_result
|
|
182
196
|
@async_handle_receiver_exceptions
|
|
183
197
|
async def async_post_appcommand(
|
|
184
198
|
self, request: str, cmds: Tuple[AppCommandCmd], cache_id: Hashable = None
|
|
@@ -201,7 +215,7 @@ class DenonAVRApi:
|
|
|
201
215
|
def add_appcommand_update_tag(self, tag: AppCommandCmd) -> None:
|
|
202
216
|
"""Add appcommand tag for full update."""
|
|
203
217
|
if tag.cmd_id != "1":
|
|
204
|
-
raise ValueError("cmd_id is {} but must be 1"
|
|
218
|
+
raise ValueError(f"cmd_id is {tag.cmd_id} but must be 1")
|
|
205
219
|
|
|
206
220
|
# Remove response pattern from tag because it is not relevant for query
|
|
207
221
|
tag = attr.evolve(tag, response_pattern=tuple())
|
|
@@ -213,7 +227,7 @@ class DenonAVRApi:
|
|
|
213
227
|
def add_appcommand0300_update_tag(self, tag: AppCommandCmd) -> None:
|
|
214
228
|
"""Add appcommand0300 tag for full update."""
|
|
215
229
|
if tag.cmd_id != "3":
|
|
216
|
-
raise ValueError("cmd_id is {} but must be 3"
|
|
230
|
+
raise ValueError(f"cmd_id is {tag.cmd_id} but must be 3")
|
|
217
231
|
|
|
218
232
|
# Remove response pattern from tag because it is not relevant for query
|
|
219
233
|
tag = attr.evolve(tag, response_pattern=tuple())
|
|
@@ -247,16 +261,20 @@ class DenonAVRApi:
|
|
|
247
261
|
"""
|
|
248
262
|
if len(cmd_list) != len(xml_root):
|
|
249
263
|
raise AvrIncompleteResponseError(
|
|
250
|
-
|
|
251
|
-
|
|
264
|
+
(
|
|
265
|
+
"Invalid length of response XML. Query has"
|
|
266
|
+
f" {len(cmd_list)} elements, response {len(xml_root)}"
|
|
267
|
+
),
|
|
252
268
|
request,
|
|
253
269
|
)
|
|
254
270
|
|
|
255
271
|
for i, child in enumerate(xml_root):
|
|
256
272
|
if child.tag not in ["cmd", "error"]:
|
|
257
273
|
raise AvrInvalidResponseError(
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
(
|
|
275
|
+
'Returned document contains a tag other than "cmd" and'
|
|
276
|
+
f' "error": {child.tag}'
|
|
277
|
+
),
|
|
260
278
|
request,
|
|
261
279
|
)
|
|
262
280
|
# Find corresponding attributes from request XML if set and add
|
|
@@ -333,7 +351,7 @@ class DenonAVRApi:
|
|
|
333
351
|
return body_bytes
|
|
334
352
|
|
|
335
353
|
def is_default_async_client(self) -> bool:
|
|
336
|
-
"""Check if default httpx.
|
|
354
|
+
"""Check if default httpx.AsyncClient getter is used."""
|
|
337
355
|
return self.async_client_getter is get_default_async_client
|
|
338
356
|
|
|
339
357
|
|
|
@@ -352,12 +370,17 @@ class DenonAVRTelnetProtocol(asyncio.Protocol):
|
|
|
352
370
|
@property
|
|
353
371
|
def connected(self) -> bool:
|
|
354
372
|
"""Return True if transport is connected."""
|
|
355
|
-
|
|
373
|
+
if self.transport is None:
|
|
374
|
+
return False
|
|
375
|
+
return not self.transport.is_closing()
|
|
356
376
|
|
|
357
377
|
def write(self, data: str) -> None:
|
|
358
378
|
"""Write data to the transport."""
|
|
359
|
-
if self.transport is
|
|
360
|
-
|
|
379
|
+
if self.transport is None:
|
|
380
|
+
return
|
|
381
|
+
if self.transport.is_closing():
|
|
382
|
+
return
|
|
383
|
+
self.transport.write(data.encode("utf-8"))
|
|
361
384
|
|
|
362
385
|
def close(self) -> None:
|
|
363
386
|
"""Close the connection."""
|
|
@@ -398,17 +421,37 @@ class DenonAVRTelnetApi:
|
|
|
398
421
|
_reconnect_task: asyncio.Task = attr.ib(default=None)
|
|
399
422
|
_monitor_handle: asyncio.TimerHandle = attr.ib(default=None)
|
|
400
423
|
_protocol: DenonAVRTelnetProtocol = attr.ib(default=None)
|
|
401
|
-
|
|
424
|
+
_telnet_event_map: Dict[str, List] = attr.ib(
|
|
425
|
+
default=attr.Factory(telnet_event_map_factory)
|
|
426
|
+
)
|
|
427
|
+
_callback_tasks: Set[asyncio.Task] = attr.ib(attr.Factory(set))
|
|
428
|
+
_send_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
|
|
429
|
+
_send_confirmation_timeout: float = attr.ib(converter=float, default=2.0)
|
|
430
|
+
_send_confirmation_event: asyncio.Event = attr.ib(
|
|
431
|
+
default=attr.Factory(asyncio.Event)
|
|
432
|
+
)
|
|
433
|
+
_send_confirmation_command: str = attr.ib(converter=str, default="")
|
|
434
|
+
_send_tasks: Set[asyncio.Task] = attr.ib(attr.Factory(set))
|
|
435
|
+
_callbacks: Dict[str, List[Coroutine]] = attr.ib(
|
|
402
436
|
validator=attr.validators.instance_of(dict),
|
|
403
437
|
default=attr.Factory(dict),
|
|
404
438
|
init=False,
|
|
405
439
|
)
|
|
440
|
+
_raw_callbacks: List[Coroutine] = attr.ib(
|
|
441
|
+
validator=attr.validators.instance_of(list),
|
|
442
|
+
default=attr.Factory(list),
|
|
443
|
+
init=False,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def __attrs_post_init__(self) -> None:
|
|
447
|
+
"""Initialize special attributes."""
|
|
448
|
+
self._register_raw_callback(self._async_send_confirmation_callback)
|
|
406
449
|
|
|
407
450
|
async def async_connect(self) -> None:
|
|
408
451
|
"""Connect to the receiver asynchronously."""
|
|
409
452
|
_LOGGER.debug("%s: telnet connecting", self.host)
|
|
410
453
|
async with self._connect_lock:
|
|
411
|
-
if self.connected
|
|
454
|
+
if self.connected:
|
|
412
455
|
return
|
|
413
456
|
await self._async_establish_connection()
|
|
414
457
|
|
|
@@ -428,27 +471,43 @@ class DenonAVRTelnetApi:
|
|
|
428
471
|
)
|
|
429
472
|
except asyncio.TimeoutError as err:
|
|
430
473
|
_LOGGER.debug("%s: Timeout exception on telnet connect", self.host)
|
|
431
|
-
raise AvrTimoutError(
|
|
432
|
-
"TimeoutException: {}".format(err), "telnet connect"
|
|
433
|
-
) from err
|
|
474
|
+
raise AvrTimoutError(f"TimeoutException: {err}", "telnet connect") from err
|
|
434
475
|
except ConnectionRefusedError as err:
|
|
435
476
|
_LOGGER.debug(
|
|
436
477
|
"%s: Connection refused on telnet connect", self.host, exc_info=True
|
|
437
478
|
)
|
|
438
479
|
raise AvrNetworkError(
|
|
439
|
-
"ConnectionRefusedError: {}"
|
|
480
|
+
f"ConnectionRefusedError: {err}", "telnet connect"
|
|
440
481
|
) from err
|
|
441
482
|
except (OSError, IOError) as err:
|
|
442
483
|
_LOGGER.debug(
|
|
443
484
|
"%s: Connection failed on telnet reconnect", self.host, exc_info=True
|
|
444
485
|
)
|
|
445
|
-
raise AvrNetworkError("OSError: {}"
|
|
486
|
+
raise AvrNetworkError(f"OSError: {err}", "telnet connect") from err
|
|
446
487
|
_LOGGER.debug("%s: telnet connection complete", self.host)
|
|
447
488
|
self._protocol = cast(DenonAVRTelnetProtocol, transport_protocol[1])
|
|
448
489
|
self._connection_enabled = True
|
|
449
490
|
self._last_message_time = time.monotonic()
|
|
450
491
|
self._schedule_monitor()
|
|
451
|
-
|
|
492
|
+
# Trigger update of all attributes
|
|
493
|
+
await self.async_send_commands(
|
|
494
|
+
"ZM?",
|
|
495
|
+
"SI?",
|
|
496
|
+
"MV?",
|
|
497
|
+
"MU?",
|
|
498
|
+
"Z2?",
|
|
499
|
+
"Z2MU?",
|
|
500
|
+
"Z3?",
|
|
501
|
+
"Z3MU?",
|
|
502
|
+
"PSTONE CTRL ?",
|
|
503
|
+
"PSBAS ?",
|
|
504
|
+
"PSTRE ?",
|
|
505
|
+
"PSDYNEQ ?",
|
|
506
|
+
"PSMULTEQ: ?",
|
|
507
|
+
"PSREFLEV ?",
|
|
508
|
+
"PSDYNVOL ?",
|
|
509
|
+
"MS?",
|
|
510
|
+
)
|
|
452
511
|
|
|
453
512
|
def _schedule_monitor(self) -> None:
|
|
454
513
|
"""Start the monitor task."""
|
|
@@ -468,7 +527,7 @@ class DenonAVRTelnetApi:
|
|
|
468
527
|
_LOGGER.info(
|
|
469
528
|
"%s: Keep alive failed, disconnecting and reconnecting", self.host
|
|
470
529
|
)
|
|
471
|
-
if self._protocol:
|
|
530
|
+
if self._protocol is not None:
|
|
472
531
|
self._protocol.close()
|
|
473
532
|
self._handle_disconnected()
|
|
474
533
|
return
|
|
@@ -511,7 +570,7 @@ class DenonAVRTelnetApi:
|
|
|
511
570
|
"""Reconnect to the receiver asynchronously."""
|
|
512
571
|
backoff = 0.5
|
|
513
572
|
|
|
514
|
-
while self._connection_enabled
|
|
573
|
+
while self._connection_enabled and not self.healthy:
|
|
515
574
|
async with self._connect_lock:
|
|
516
575
|
try:
|
|
517
576
|
await self._async_establish_connection()
|
|
@@ -539,8 +598,8 @@ class DenonAVRTelnetApi:
|
|
|
539
598
|
) -> None:
|
|
540
599
|
"""Register a callback handler for an event type."""
|
|
541
600
|
# Validate the passed in type
|
|
542
|
-
if event !=
|
|
543
|
-
raise ValueError("{} is not a valid callback type."
|
|
601
|
+
if event != ALL_TELNET_EVENTS and event not in TELNET_EVENTS:
|
|
602
|
+
raise ValueError(f"{event} is not a valid callback type.")
|
|
544
603
|
|
|
545
604
|
if event not in self._callbacks.keys():
|
|
546
605
|
self._callbacks[event] = []
|
|
@@ -554,18 +613,29 @@ class DenonAVRTelnetApi:
|
|
|
554
613
|
return
|
|
555
614
|
self._callbacks[event].remove(callback)
|
|
556
615
|
|
|
616
|
+
def _register_raw_callback(
|
|
617
|
+
self, callback: Callable[[str], Awaitable[None]]
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Register a callback handler for raw telnet messages."""
|
|
620
|
+
self._raw_callbacks.append(callback)
|
|
621
|
+
|
|
622
|
+
def _unregister_raw_callback(
|
|
623
|
+
self, callback: Callable[[str], Awaitable[None]]
|
|
624
|
+
) -> None:
|
|
625
|
+
"""Unregister a callback handler for raw telnet messages."""
|
|
626
|
+
self._raw_callbacks.remove(callback)
|
|
627
|
+
|
|
557
628
|
def _process_event(self, message: str) -> None:
|
|
558
629
|
"""Process a realtime event."""
|
|
559
630
|
_LOGGER.debug("Incoming Telnet message: %s", message)
|
|
560
631
|
self._last_message_time = time.monotonic()
|
|
561
632
|
if len(message) < 3:
|
|
562
633
|
return
|
|
563
|
-
zone = MAIN_ZONE
|
|
564
634
|
|
|
565
635
|
# Event is 2 characters
|
|
566
|
-
event = message
|
|
636
|
+
event = self._get_event(message)
|
|
567
637
|
# Parameter is the remaining characters
|
|
568
|
-
parameter = message[
|
|
638
|
+
parameter = message[len(event) :]
|
|
569
639
|
|
|
570
640
|
if event == "MV":
|
|
571
641
|
# This seems undocumented by Denon and appears to basically be a
|
|
@@ -574,29 +644,49 @@ class DenonAVRTelnetApi:
|
|
|
574
644
|
if parameter[0:3] == "MAX":
|
|
575
645
|
return
|
|
576
646
|
|
|
577
|
-
|
|
647
|
+
# Determine zone
|
|
648
|
+
zone = MAIN_ZONE
|
|
649
|
+
if event in ALL_ZONE_TELNET_EVENTS:
|
|
650
|
+
zone = ALL_ZONES
|
|
651
|
+
elif event in {"Z2", "Z3"}:
|
|
578
652
|
if event == "Z2":
|
|
579
653
|
zone = ZONE2
|
|
580
654
|
else:
|
|
581
655
|
zone = ZONE3
|
|
582
656
|
|
|
583
|
-
if parameter in
|
|
584
|
-
event = "PW"
|
|
585
|
-
elif parameter in TELNET_SOURCES:
|
|
657
|
+
if parameter in TELNET_SOURCES:
|
|
586
658
|
event = "SI"
|
|
587
659
|
elif parameter.isdigit():
|
|
588
660
|
event = "MV"
|
|
589
|
-
elif parameter
|
|
590
|
-
event = parameter
|
|
591
|
-
parameter = parameter[
|
|
661
|
+
elif self._get_event(parameter):
|
|
662
|
+
event = self._get_event(parameter)
|
|
663
|
+
parameter = parameter[len(event) :]
|
|
592
664
|
|
|
593
665
|
if event not in TELNET_EVENTS:
|
|
594
666
|
return
|
|
595
667
|
|
|
596
|
-
asyncio.create_task(
|
|
668
|
+
task = asyncio.create_task(
|
|
669
|
+
self._async_run_callbacks(message, event, zone, parameter)
|
|
670
|
+
)
|
|
671
|
+
self._callback_tasks.add(task)
|
|
672
|
+
task.add_done_callback(self._callback_tasks.discard)
|
|
673
|
+
|
|
674
|
+
async def _async_run_callbacks(
|
|
675
|
+
self, message: str, event: str, zone: str, parameter: str
|
|
676
|
+
) -> None:
|
|
677
|
+
"""Handle triggering the registered callbacks."""
|
|
678
|
+
for callback in self._raw_callbacks:
|
|
679
|
+
try:
|
|
680
|
+
await callback(message)
|
|
681
|
+
except Exception as err: # pylint: disable=broad-except
|
|
682
|
+
# We don't want a single bad callback to trip up the
|
|
683
|
+
# whole system and prevent further execution
|
|
684
|
+
_LOGGER.error(
|
|
685
|
+
"%s: Raw callback caused an unhandled exception %s",
|
|
686
|
+
self.host,
|
|
687
|
+
err,
|
|
688
|
+
)
|
|
597
689
|
|
|
598
|
-
async def _async_run_callbacks(self, event: str, zone: str, parameter: str) -> None:
|
|
599
|
-
"""Handle triggering the registered callbacks for the event."""
|
|
600
690
|
if event in self._callbacks.keys():
|
|
601
691
|
for callback in self._callbacks[event]:
|
|
602
692
|
try:
|
|
@@ -610,8 +700,8 @@ class DenonAVRTelnetApi:
|
|
|
610
700
|
err,
|
|
611
701
|
)
|
|
612
702
|
|
|
613
|
-
if
|
|
614
|
-
for callback in self._callbacks[
|
|
703
|
+
if ALL_TELNET_EVENTS in self._callbacks.keys():
|
|
704
|
+
for callback in self._callbacks[ALL_TELNET_EVENTS]:
|
|
615
705
|
try:
|
|
616
706
|
await callback(zone, event, parameter)
|
|
617
707
|
except Exception as err: # pylint: disable=broad-except
|
|
@@ -623,10 +713,55 @@ class DenonAVRTelnetApi:
|
|
|
623
713
|
err,
|
|
624
714
|
)
|
|
625
715
|
|
|
626
|
-
def
|
|
716
|
+
def _get_event(self, message: str) -> str:
|
|
717
|
+
"""Get event of a telnet message."""
|
|
718
|
+
events = self._telnet_event_map.get(message[0:2], [""])
|
|
719
|
+
for event in events:
|
|
720
|
+
if message.startswith(event):
|
|
721
|
+
return event
|
|
722
|
+
return ""
|
|
723
|
+
|
|
724
|
+
async def _async_send_confirmation_callback(self, message: str) -> None:
|
|
725
|
+
"""Confirm that the telnet command has been executed."""
|
|
726
|
+
if len(message) < 3:
|
|
727
|
+
return
|
|
728
|
+
command = self._send_confirmation_command
|
|
729
|
+
if self._get_event(message) == self._get_event(self._send_confirmation_command):
|
|
730
|
+
self._send_confirmation_command = ""
|
|
731
|
+
self._send_confirmation_event.set()
|
|
732
|
+
_LOGGER.debug("Command %s confirmed", command)
|
|
733
|
+
|
|
734
|
+
async def _async_send_command(self, command: str) -> None:
|
|
735
|
+
"""Send one telnet command to the receiver."""
|
|
736
|
+
async with self._send_lock:
|
|
737
|
+
self._send_confirmation_command = command
|
|
738
|
+
self._send_confirmation_event.clear()
|
|
739
|
+
if not self.connected or not self.healthy:
|
|
740
|
+
raise AvrProcessingError(
|
|
741
|
+
f"Error sending command {command}. Telnet connected: "
|
|
742
|
+
f"{self.connected}, Connection healthy: {self.healthy}"
|
|
743
|
+
)
|
|
744
|
+
self._protocol.write(f"{command}\r")
|
|
745
|
+
try:
|
|
746
|
+
await asyncio.wait_for(
|
|
747
|
+
self._send_confirmation_event.wait(),
|
|
748
|
+
self._send_confirmation_timeout,
|
|
749
|
+
)
|
|
750
|
+
except asyncio.TimeoutError:
|
|
751
|
+
_LOGGER.info("Timeout waiting for confirmation of command: %s", command)
|
|
752
|
+
finally:
|
|
753
|
+
self._send_confirmation_command = ""
|
|
754
|
+
|
|
755
|
+
async def async_send_commands(self, *commands: str) -> None:
|
|
627
756
|
"""Send telnet commands to the receiver."""
|
|
628
757
|
for command in commands:
|
|
629
|
-
self.
|
|
758
|
+
await self._async_send_command(command)
|
|
759
|
+
|
|
760
|
+
def send_commands(self, *commands: str) -> None:
|
|
761
|
+
"""Send telnet commands to the receiver."""
|
|
762
|
+
task = asyncio.create_task(self.async_send_commands(*commands))
|
|
763
|
+
self._send_tasks.add(task)
|
|
764
|
+
task.add_done_callback(self._send_tasks.discard)
|
|
630
765
|
|
|
631
766
|
##############
|
|
632
767
|
# Properties #
|
|
@@ -637,6 +772,6 @@ class DenonAVRTelnetApi:
|
|
|
637
772
|
return self._connection_enabled
|
|
638
773
|
|
|
639
774
|
@property
|
|
640
|
-
def healthy(self) ->
|
|
775
|
+
def healthy(self) -> bool:
|
|
641
776
|
"""Return True if telnet connection is healthy."""
|
|
642
777
|
return self._protocol is not None and self._protocol.connected
|
denonavr/audyssey.py
CHANGED
|
@@ -8,7 +8,8 @@ This module implements the Audyssey settings of Denon AVR receivers.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
-
from
|
|
11
|
+
from collections.abc import Hashable
|
|
12
|
+
from typing import List, Optional
|
|
12
13
|
|
|
13
14
|
import attr
|
|
14
15
|
|
|
@@ -16,11 +17,14 @@ from .appcommand import AppCommandCmd, AppCommandCmdParam, AppCommands
|
|
|
16
17
|
from .const import (
|
|
17
18
|
DENON_ATTR_SETATTR,
|
|
18
19
|
DYNAMIC_VOLUME_MAP,
|
|
19
|
-
|
|
20
|
+
DYNAMIC_VOLUME_MAP_LABELS_APPCOMMAND,
|
|
21
|
+
DYNAMIC_VOLUME_MAP_LABELS_TELNET,
|
|
20
22
|
MULTI_EQ_MAP,
|
|
21
|
-
|
|
23
|
+
MULTI_EQ_MAP_LABELS_APPCOMMAND,
|
|
24
|
+
MULTI_EQ_MAP_LABELS_TELNET,
|
|
22
25
|
REF_LVL_OFFSET_MAP,
|
|
23
|
-
|
|
26
|
+
REF_LVL_OFFSET_MAP_LABELS_APPCOMMAND,
|
|
27
|
+
REF_LVL_OFFSET_MAP_LABELS_TELNET,
|
|
24
28
|
)
|
|
25
29
|
from .exceptions import AvrCommandError, AvrProcessingError
|
|
26
30
|
from .foundation import DenonAVRFoundation, convert_string_int_bool
|
|
@@ -96,7 +100,7 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
96
100
|
) -> None:
|
|
97
101
|
"""Update Audyssey asynchronously."""
|
|
98
102
|
# Ensure instance is setup before updating
|
|
99
|
-
if self._is_setup
|
|
103
|
+
if not self._is_setup:
|
|
100
104
|
self.setup()
|
|
101
105
|
|
|
102
106
|
# Update state
|
|
@@ -106,7 +110,13 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
106
110
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
107
111
|
):
|
|
108
112
|
"""Update Audyssey status of device."""
|
|
109
|
-
if self._device.use_avr_2016_update is
|
|
113
|
+
if self._device.use_avr_2016_update is None:
|
|
114
|
+
raise AvrProcessingError(
|
|
115
|
+
"Device is not setup correctly, update method not set"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Audyssey is only available for avr 2016 update
|
|
119
|
+
if self._device.use_avr_2016_update:
|
|
110
120
|
await self.async_update_attrs_appcommand(
|
|
111
121
|
self.appcommand0300_attrs,
|
|
112
122
|
appcommand0300=True,
|
|
@@ -114,13 +124,6 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
114
124
|
cache_id=cache_id,
|
|
115
125
|
ignore_missing_response=True,
|
|
116
126
|
)
|
|
117
|
-
elif self._device.use_avr_2016_update is False:
|
|
118
|
-
# Not available
|
|
119
|
-
pass
|
|
120
|
-
else:
|
|
121
|
-
raise AvrProcessingError(
|
|
122
|
-
"Device is not setup correctly, update method not set"
|
|
123
|
-
)
|
|
124
127
|
|
|
125
128
|
async def _async_set_audyssey(self, cmd: AppCommandCmd) -> None:
|
|
126
129
|
"""Set Audyssey parameter."""
|
|
@@ -130,13 +133,9 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
130
133
|
|
|
131
134
|
try:
|
|
132
135
|
if res.find("cmd").text != "OK":
|
|
133
|
-
raise AvrProcessingError(
|
|
134
|
-
"SetAudyssey command {} failed".format(cmd.name)
|
|
135
|
-
)
|
|
136
|
+
raise AvrProcessingError(f"SetAudyssey command {cmd.name} failed")
|
|
136
137
|
except AttributeError as err:
|
|
137
|
-
raise AvrProcessingError(
|
|
138
|
-
"SetAudyssey command {} failed".format(cmd.name)
|
|
139
|
-
) from err
|
|
138
|
+
raise AvrProcessingError(f"SetAudyssey command {cmd.name} failed") from err
|
|
140
139
|
|
|
141
140
|
##############
|
|
142
141
|
# Properties #
|
|
@@ -154,7 +153,9 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
154
153
|
@property
|
|
155
154
|
def reference_level_offset_setting_list(self) -> List[str]:
|
|
156
155
|
"""Return a list of available reference level offset settings."""
|
|
157
|
-
|
|
156
|
+
if self._device.telnet_available:
|
|
157
|
+
return list(REF_LVL_OFFSET_MAP_LABELS_TELNET.keys())
|
|
158
|
+
return list(REF_LVL_OFFSET_MAP_LABELS_APPCOMMAND.keys())
|
|
158
159
|
|
|
159
160
|
@property
|
|
160
161
|
def dynamic_volume(self) -> Optional[str]:
|
|
@@ -164,7 +165,9 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
164
165
|
@property
|
|
165
166
|
def dynamic_volume_setting_list(self) -> List[str]:
|
|
166
167
|
"""Return a list of available Dynamic Volume settings."""
|
|
167
|
-
|
|
168
|
+
if self._device.telnet_available:
|
|
169
|
+
return list(DYNAMIC_VOLUME_MAP_LABELS_TELNET.keys())
|
|
170
|
+
return list(DYNAMIC_VOLUME_MAP_LABELS_APPCOMMAND.keys())
|
|
168
171
|
|
|
169
172
|
@property
|
|
170
173
|
def multi_eq(self) -> Optional[str]:
|
|
@@ -174,13 +177,19 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
174
177
|
@property
|
|
175
178
|
def multi_eq_setting_list(self) -> List[str]:
|
|
176
179
|
"""Return a list of available MultiEQ settings."""
|
|
177
|
-
|
|
180
|
+
if self._device.telnet_available:
|
|
181
|
+
return list(MULTI_EQ_MAP_LABELS_TELNET.keys())
|
|
182
|
+
return list(MULTI_EQ_MAP_LABELS_APPCOMMAND.keys())
|
|
178
183
|
|
|
179
184
|
##########
|
|
180
185
|
# Setter #
|
|
181
186
|
##########
|
|
182
187
|
async def async_dynamiceq_off(self) -> None:
|
|
183
188
|
"""Turn DynamicEQ off."""
|
|
189
|
+
if self._device.telnet_available:
|
|
190
|
+
telnet_command = self._device.telnet_commands.command_dynamiceq + "OFF"
|
|
191
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
192
|
+
return
|
|
184
193
|
cmd = attr.evolve(
|
|
185
194
|
AppCommands.SetAudysseyDynamicEQ,
|
|
186
195
|
param_list=(AppCommandCmdParam(name="dynamiceq", text=0),),
|
|
@@ -189,6 +198,10 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
189
198
|
|
|
190
199
|
async def async_dynamiceq_on(self) -> None:
|
|
191
200
|
"""Turn DynamicEQ on."""
|
|
201
|
+
if self._device.telnet_available:
|
|
202
|
+
telnet_command = self._device.telnet_commands.command_dynamiceq + "ON"
|
|
203
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
204
|
+
return
|
|
192
205
|
cmd = attr.evolve(
|
|
193
206
|
AppCommands.SetAudysseyDynamicEQ,
|
|
194
207
|
param_list=(AppCommandCmdParam(name="dynamiceq", text=1),),
|
|
@@ -197,9 +210,17 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
197
210
|
|
|
198
211
|
async def async_set_multieq(self, value: str) -> None:
|
|
199
212
|
"""Set MultiEQ mode."""
|
|
200
|
-
|
|
213
|
+
if self._device.telnet_available:
|
|
214
|
+
setting = MULTI_EQ_MAP_LABELS_TELNET.get(value)
|
|
215
|
+
if setting is None:
|
|
216
|
+
raise AvrCommandError(f"Value {value} not known for MultiEQ")
|
|
217
|
+
telnet_command = self._device.telnet_commands.command_multieq + setting
|
|
218
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
setting = MULTI_EQ_MAP_LABELS_APPCOMMAND.get(value)
|
|
201
222
|
if setting is None:
|
|
202
|
-
raise AvrCommandError("Value {} not known for MultiEQ"
|
|
223
|
+
raise AvrCommandError(f"Value {value} not known for MultiEQ")
|
|
203
224
|
cmd = attr.evolve(
|
|
204
225
|
AppCommands.SetAudysseyMultiEQ,
|
|
205
226
|
param_list=(AppCommandCmdParam(name="multeq", text=setting),),
|
|
@@ -209,15 +230,23 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
209
230
|
async def async_set_reflevoffset(self, value: str) -> None:
|
|
210
231
|
"""Set Reference Level Offset."""
|
|
211
232
|
# Reference level offset can only be used with DynamicEQ
|
|
212
|
-
if self._dynamiceq
|
|
233
|
+
if not self._dynamiceq:
|
|
213
234
|
raise AvrCommandError(
|
|
214
235
|
"Reference level could only be set when DynamicEQ is active"
|
|
215
236
|
)
|
|
216
|
-
|
|
237
|
+
if self._device.telnet_available:
|
|
238
|
+
setting = REF_LVL_OFFSET_MAP_LABELS_TELNET.get(value)
|
|
239
|
+
if setting is None:
|
|
240
|
+
raise AvrCommandError(
|
|
241
|
+
f"Value {value} not known for Reference level offset"
|
|
242
|
+
)
|
|
243
|
+
telnet_command = self._device.telnet_commands.command_reflevoffset + setting
|
|
244
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
setting = REF_LVL_OFFSET_MAP_LABELS_APPCOMMAND.get(value)
|
|
217
248
|
if setting is None:
|
|
218
|
-
raise AvrCommandError(
|
|
219
|
-
"Value {} not known for Reference level offset".format(value)
|
|
220
|
-
)
|
|
249
|
+
raise AvrCommandError(f"Value {value} not known for Reference level offset")
|
|
221
250
|
cmd = attr.evolve(
|
|
222
251
|
AppCommands.SetAudysseyReflevoffset,
|
|
223
252
|
param_list=(AppCommandCmdParam(name="reflevoffset", text=setting),),
|
|
@@ -226,9 +255,17 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
226
255
|
|
|
227
256
|
async def async_set_dynamicvol(self, value: str) -> None:
|
|
228
257
|
"""Set Dynamic Volume."""
|
|
229
|
-
|
|
258
|
+
if self._device.telnet_available:
|
|
259
|
+
setting = DYNAMIC_VOLUME_MAP_LABELS_TELNET.get(value)
|
|
260
|
+
if setting is None:
|
|
261
|
+
raise AvrCommandError(f"Value {value} not known for Dynamic Volume")
|
|
262
|
+
telnet_command = self._device.telnet_commands.command_dynamicvol + setting
|
|
263
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
setting = DYNAMIC_VOLUME_MAP_LABELS_APPCOMMAND.get(value)
|
|
230
267
|
if setting is None:
|
|
231
|
-
raise AvrCommandError("Value {} not known for Dynamic Volume"
|
|
268
|
+
raise AvrCommandError(f"Value {value} not known for Dynamic Volume")
|
|
232
269
|
cmd = attr.evolve(
|
|
233
270
|
AppCommands.SetAudysseyDynamicvol,
|
|
234
271
|
param_list=(AppCommandCmdParam(name="dynamicvol", text=setting),),
|
|
@@ -237,7 +274,7 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
237
274
|
|
|
238
275
|
async def async_toggle_dynamic_eq(self) -> None:
|
|
239
276
|
"""Toggle DynamicEQ."""
|
|
240
|
-
if self._dynamiceq
|
|
277
|
+
if self._dynamiceq:
|
|
241
278
|
await self.async_dynamiceq_off()
|
|
242
279
|
else:
|
|
243
280
|
await self.async_dynamiceq_on()
|