denonavr 0.11.6__py3-none-any.whl → 1.0.1__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 -24
- denonavr/api.py +181 -94
- denonavr/audyssey.py +12 -7
- denonavr/const.py +168 -59
- denonavr/decorators.py +9 -76
- denonavr/denonavr.py +47 -187
- denonavr/foundation.py +126 -40
- denonavr/input.py +57 -9
- denonavr/soundmode.py +51 -36
- denonavr/tonecontrol.py +47 -12
- denonavr/volume.py +4 -2
- denonavr-1.0.1.dist-info/METADATA +158 -0
- denonavr-1.0.1.dist-info/RECORD +19 -0
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/WHEEL +1 -1
- denonavr-0.11.6.dist-info/METADATA +0 -290
- denonavr-0.11.6.dist-info/RECORD +0 -19
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/LICENSE +0 -0
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/top_level.txt +0 -0
denonavr/__init__.py
CHANGED
|
@@ -10,8 +10,6 @@ Automation Library for Denon AVR receivers.
|
|
|
10
10
|
# Set default logging handler to avoid "No handler found" warnings.
|
|
11
11
|
import logging
|
|
12
12
|
|
|
13
|
-
from .decorators import run_async_synchronously
|
|
14
|
-
|
|
15
13
|
# Import denonavr module
|
|
16
14
|
from .denonavr import DenonAVR
|
|
17
15
|
from .ssdp import async_identify_denonavr_receivers
|
|
@@ -19,7 +17,7 @@ from .ssdp import async_identify_denonavr_receivers
|
|
|
19
17
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
20
18
|
|
|
21
19
|
__title__ = "denonavr"
|
|
22
|
-
__version__ = "0.
|
|
20
|
+
__version__ = "1.0.1"
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
async def async_discover():
|
|
@@ -33,17 +31,6 @@ async def async_discover():
|
|
|
33
31
|
return await async_identify_denonavr_receivers()
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
@run_async_synchronously(async_func=async_discover)
|
|
37
|
-
def discover():
|
|
38
|
-
"""
|
|
39
|
-
Discover all Denon AVR devices in LAN zone.
|
|
40
|
-
|
|
41
|
-
Returns a list of dictionaries which includes all discovered Denon AVR
|
|
42
|
-
devices with keys "host", "modelName", "friendlyName", "presentationURL".
|
|
43
|
-
By default SSDP broadcasts are sent once with a 2 seconds timeout.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
34
|
async def async_init_all_receivers():
|
|
48
35
|
"""
|
|
49
36
|
Initialize all discovered Denon AVR receivers in LAN zone.
|
|
@@ -58,13 +45,3 @@ async def async_init_all_receivers():
|
|
|
58
45
|
init_receiver = DenonAVR(receiver["host"])
|
|
59
46
|
init_receivers.append(init_receiver)
|
|
60
47
|
return init_receivers
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@run_async_synchronously(async_func=async_init_all_receivers)
|
|
64
|
-
def init_all_receivers():
|
|
65
|
-
"""
|
|
66
|
-
Initialize all discovered Denon AVR receivers in LAN zone.
|
|
67
|
-
|
|
68
|
-
Returns a list of created Denon AVR instances.
|
|
69
|
-
By default SSDP broadcasts are sent up to 3 times with a 2 seconds timeout.
|
|
70
|
-
"""
|
denonavr/api.py
CHANGED
|
@@ -31,7 +31,8 @@ from typing import (
|
|
|
31
31
|
|
|
32
32
|
import attr
|
|
33
33
|
import httpx
|
|
34
|
-
from defusedxml
|
|
34
|
+
from defusedxml import DefusedXmlException
|
|
35
|
+
from defusedxml.ElementTree import ParseError, fromstring
|
|
35
36
|
|
|
36
37
|
from .appcommand import AppCommandCmd
|
|
37
38
|
from .const import (
|
|
@@ -83,16 +84,86 @@ def telnet_event_map_factory() -> Dict[str, List]:
|
|
|
83
84
|
return dict(event_map)
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
@attr.s(auto_attribs=True, hash=False
|
|
87
|
+
@attr.s(auto_attribs=True, hash=False)
|
|
88
|
+
class HTTPXAsyncClient:
|
|
89
|
+
"""Perform cached HTTP calls with httpx.AsyncClient."""
|
|
90
|
+
|
|
91
|
+
client_getter: Callable[[], httpx.AsyncClient] = attr.ib(
|
|
92
|
+
validator=attr.validators.is_callable(),
|
|
93
|
+
default=get_default_async_client,
|
|
94
|
+
init=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __hash__(self) -> int:
|
|
98
|
+
"""Hash the class using its ID that caching works."""
|
|
99
|
+
return id(self)
|
|
100
|
+
|
|
101
|
+
@cache_result
|
|
102
|
+
@async_handle_receiver_exceptions
|
|
103
|
+
async def async_get(
|
|
104
|
+
self,
|
|
105
|
+
url: str,
|
|
106
|
+
timeout: float,
|
|
107
|
+
read_timeout: float,
|
|
108
|
+
*,
|
|
109
|
+
cache_id: Hashable = None,
|
|
110
|
+
) -> httpx.Response:
|
|
111
|
+
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
112
|
+
client = self.client_getter()
|
|
113
|
+
try:
|
|
114
|
+
res = await client.get(
|
|
115
|
+
url, timeout=httpx.Timeout(timeout, read=read_timeout)
|
|
116
|
+
)
|
|
117
|
+
res.raise_for_status()
|
|
118
|
+
finally:
|
|
119
|
+
# Close the default AsyncClient but keep custom clients open
|
|
120
|
+
if self.is_default_async_client():
|
|
121
|
+
await client.aclose()
|
|
122
|
+
|
|
123
|
+
return res
|
|
124
|
+
|
|
125
|
+
@cache_result
|
|
126
|
+
@async_handle_receiver_exceptions
|
|
127
|
+
async def async_post(
|
|
128
|
+
self,
|
|
129
|
+
url: str,
|
|
130
|
+
timeout: float,
|
|
131
|
+
read_timeout: float,
|
|
132
|
+
*,
|
|
133
|
+
content: Optional[bytes] = None,
|
|
134
|
+
data: Optional[Dict] = None,
|
|
135
|
+
cache_id: Hashable = None,
|
|
136
|
+
) -> httpx.Response:
|
|
137
|
+
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
138
|
+
client = self.client_getter()
|
|
139
|
+
try:
|
|
140
|
+
res = await client.post(
|
|
141
|
+
url,
|
|
142
|
+
content=content,
|
|
143
|
+
data=data,
|
|
144
|
+
timeout=httpx.Timeout(timeout, read=read_timeout),
|
|
145
|
+
)
|
|
146
|
+
res.raise_for_status()
|
|
147
|
+
finally:
|
|
148
|
+
# Close the default AsyncClient but keep custom clients open
|
|
149
|
+
if self.is_default_async_client():
|
|
150
|
+
await client.aclose()
|
|
151
|
+
|
|
152
|
+
return res
|
|
153
|
+
|
|
154
|
+
def is_default_async_client(self) -> bool:
|
|
155
|
+
"""Check if default httpx.AsyncClient getter is used."""
|
|
156
|
+
return self.client_getter is get_default_async_client
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR)
|
|
87
160
|
class DenonAVRApi:
|
|
88
161
|
"""Perform API calls to Denon AVR REST interface."""
|
|
89
162
|
|
|
90
163
|
host: str = attr.ib(converter=str, default="localhost")
|
|
91
164
|
port: int = attr.ib(converter=int, default=80)
|
|
92
|
-
timeout:
|
|
93
|
-
|
|
94
|
-
default=httpx.Timeout(2.0, read=15.0),
|
|
95
|
-
)
|
|
165
|
+
timeout: float = attr.ib(converter=float, default=2.0)
|
|
166
|
+
read_timeout: float = attr.ib(converter=float, default=15.0)
|
|
96
167
|
_appcommand_update_tags: Tuple[AppCommandCmd] = attr.ib(
|
|
97
168
|
validator=attr.validators.deep_iterable(
|
|
98
169
|
attr.validators.instance_of(AppCommandCmd),
|
|
@@ -107,23 +178,18 @@ class DenonAVRApi:
|
|
|
107
178
|
),
|
|
108
179
|
default=attr.Factory(tuple),
|
|
109
180
|
)
|
|
110
|
-
|
|
111
|
-
validator=attr.validators.
|
|
112
|
-
default=
|
|
181
|
+
httpx_async_client: HTTPXAsyncClient = attr.ib(
|
|
182
|
+
validator=attr.validators.instance_of(HTTPXAsyncClient),
|
|
183
|
+
default=attr.Factory(HTTPXAsyncClient),
|
|
113
184
|
init=False,
|
|
114
185
|
)
|
|
115
186
|
|
|
116
|
-
def __hash__(self) -> int:
|
|
117
|
-
"""
|
|
118
|
-
Hash the class in a custom way that caching works.
|
|
119
|
-
|
|
120
|
-
It should react on changes of host and port.
|
|
121
|
-
"""
|
|
122
|
-
return hash((self.host, self.port))
|
|
123
|
-
|
|
124
|
-
@async_handle_receiver_exceptions
|
|
125
187
|
async def async_get(
|
|
126
|
-
self,
|
|
188
|
+
self,
|
|
189
|
+
request: str,
|
|
190
|
+
*,
|
|
191
|
+
port: Optional[int] = None,
|
|
192
|
+
cache_id: Hashable = None,
|
|
127
193
|
) -> httpx.Response:
|
|
128
194
|
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
129
195
|
# Use default port of the receiver if no different port is specified
|
|
@@ -131,24 +197,18 @@ class DenonAVRApi:
|
|
|
131
197
|
|
|
132
198
|
endpoint = f"http://{self.host}:{port}{request}"
|
|
133
199
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
res.raise_for_status()
|
|
138
|
-
finally:
|
|
139
|
-
# Close the default AsyncClient but keep custom clients open
|
|
140
|
-
if self.is_default_async_client():
|
|
141
|
-
await client.aclose()
|
|
142
|
-
|
|
143
|
-
return res
|
|
200
|
+
return await self.httpx_async_client.async_get(
|
|
201
|
+
endpoint, self.timeout, self.read_timeout, cache_id=cache_id
|
|
202
|
+
)
|
|
144
203
|
|
|
145
|
-
@async_handle_receiver_exceptions
|
|
146
204
|
async def async_post(
|
|
147
205
|
self,
|
|
148
206
|
request: str,
|
|
207
|
+
*,
|
|
149
208
|
content: Optional[bytes] = None,
|
|
150
209
|
data: Optional[Dict] = None,
|
|
151
210
|
port: Optional[int] = None,
|
|
211
|
+
cache_id: Hashable = None,
|
|
152
212
|
) -> httpx.Response:
|
|
153
213
|
"""Call POST endpoint of Denon AVR receiver asynchronously."""
|
|
154
214
|
# Use default port of the receiver if no different port is specified
|
|
@@ -156,20 +216,15 @@ class DenonAVRApi:
|
|
|
156
216
|
|
|
157
217
|
endpoint = f"http://{self.host}:{port}{request}"
|
|
158
218
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if self.is_default_async_client():
|
|
168
|
-
await client.aclose()
|
|
169
|
-
|
|
170
|
-
return res
|
|
219
|
+
return await self.httpx_async_client.async_post(
|
|
220
|
+
endpoint,
|
|
221
|
+
self.timeout,
|
|
222
|
+
self.read_timeout,
|
|
223
|
+
content=content,
|
|
224
|
+
data=data,
|
|
225
|
+
cache_id=cache_id,
|
|
226
|
+
)
|
|
171
227
|
|
|
172
|
-
@async_handle_receiver_exceptions
|
|
173
228
|
async def async_get_command(self, request: str) -> str:
|
|
174
229
|
"""Send HTTP GET command to Denon AVR receiver asynchronously."""
|
|
175
230
|
# HTTP GET to endpoint
|
|
@@ -177,34 +232,46 @@ class DenonAVRApi:
|
|
|
177
232
|
# Return text
|
|
178
233
|
return res.text
|
|
179
234
|
|
|
180
|
-
@cache_result
|
|
181
|
-
@async_handle_receiver_exceptions
|
|
182
235
|
async def async_get_xml(
|
|
183
|
-
self, request: str, cache_id: Hashable = None
|
|
236
|
+
self, request: str, *, cache_id: Hashable = None
|
|
184
237
|
) -> ET.Element:
|
|
185
238
|
"""Return XML data from HTTP GET endpoint asynchronously."""
|
|
186
239
|
# HTTP GET to endpoint
|
|
187
|
-
res = await self.async_get(request)
|
|
240
|
+
res = await self.async_get(request, cache_id=cache_id)
|
|
188
241
|
# create ElementTree
|
|
189
|
-
|
|
242
|
+
try:
|
|
243
|
+
xml_root = fromstring(res.text)
|
|
244
|
+
except (
|
|
245
|
+
ET.ParseError,
|
|
246
|
+
DefusedXmlException,
|
|
247
|
+
ParseError,
|
|
248
|
+
UnicodeDecodeError,
|
|
249
|
+
) as err:
|
|
250
|
+
raise AvrInvalidResponseError(f"XMLParseError: {err}", request) from err
|
|
190
251
|
# Check validity of XML
|
|
191
252
|
self.check_xml_validity(request, xml_root)
|
|
192
253
|
# Return ElementTree element
|
|
193
254
|
return xml_root
|
|
194
255
|
|
|
195
|
-
@cache_result
|
|
196
|
-
@async_handle_receiver_exceptions
|
|
197
256
|
async def async_post_appcommand(
|
|
198
|
-
self, request: str, cmds: Tuple[AppCommandCmd], cache_id: Hashable = None
|
|
257
|
+
self, request: str, cmds: Tuple[AppCommandCmd], *, cache_id: Hashable = None
|
|
199
258
|
) -> ET.Element:
|
|
200
259
|
"""Return XML from Appcommand(0300) endpoint asynchronously."""
|
|
201
260
|
# Prepare XML body for POST call
|
|
202
261
|
content = self.prepare_appcommand_body(cmds)
|
|
203
262
|
_LOGGER.debug("Content for %s endpoint: %s", request, content)
|
|
204
263
|
# HTTP POST to endpoint
|
|
205
|
-
res = await self.async_post(request, content=content)
|
|
264
|
+
res = await self.async_post(request, content=content, cache_id=cache_id)
|
|
206
265
|
# create ElementTree
|
|
207
|
-
|
|
266
|
+
try:
|
|
267
|
+
xml_root = fromstring(res.text)
|
|
268
|
+
except (
|
|
269
|
+
ET.ParseError,
|
|
270
|
+
DefusedXmlException,
|
|
271
|
+
ParseError,
|
|
272
|
+
UnicodeDecodeError,
|
|
273
|
+
) as err:
|
|
274
|
+
raise AvrInvalidResponseError(f"XMLParseError: {err}", request) from err
|
|
208
275
|
# Check validity of XML
|
|
209
276
|
self.check_xml_validity(request, xml_root)
|
|
210
277
|
# Add query tags to result
|
|
@@ -350,10 +417,6 @@ class DenonAVRApi:
|
|
|
350
417
|
|
|
351
418
|
return body_bytes
|
|
352
419
|
|
|
353
|
-
def is_default_async_client(self) -> bool:
|
|
354
|
-
"""Check if default httpx.AsyncClient getter is used."""
|
|
355
|
-
return self.async_client_getter is get_default_async_client
|
|
356
|
-
|
|
357
420
|
|
|
358
421
|
class DenonAVRTelnetProtocol(asyncio.Protocol):
|
|
359
422
|
"""Protocol for the Denon AVR Telnet interface."""
|
|
@@ -401,9 +464,9 @@ class DenonAVRTelnetProtocol(asyncio.Protocol):
|
|
|
401
464
|
|
|
402
465
|
def connection_lost(self, exc: Optional[Exception]) -> None:
|
|
403
466
|
"""Handle connection lost."""
|
|
404
|
-
self.transport = None
|
|
405
467
|
self._on_connection_lost()
|
|
406
|
-
|
|
468
|
+
super().connection_lost(exc)
|
|
469
|
+
self.transport = None
|
|
407
470
|
|
|
408
471
|
|
|
409
472
|
@attr.s(auto_attribs=True, hash=False, on_setattr=DENON_ATTR_SETATTR)
|
|
@@ -413,9 +476,6 @@ class DenonAVRTelnetApi:
|
|
|
413
476
|
host: str = attr.ib(converter=str, default="localhost")
|
|
414
477
|
timeout: float = attr.ib(converter=float, default=2.0)
|
|
415
478
|
_connection_enabled: bool = attr.ib(default=False)
|
|
416
|
-
_healthy: Optional[bool] = attr.ib(
|
|
417
|
-
converter=attr.converters.optional(bool), default=None
|
|
418
|
-
)
|
|
419
479
|
_last_message_time: float = attr.ib(default=-1.0)
|
|
420
480
|
_connect_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
|
|
421
481
|
_reconnect_task: asyncio.Task = attr.ib(default=None)
|
|
@@ -474,18 +534,18 @@ class DenonAVRTelnetApi:
|
|
|
474
534
|
raise AvrTimoutError(f"TimeoutException: {err}", "telnet connect") from err
|
|
475
535
|
except ConnectionRefusedError as err:
|
|
476
536
|
_LOGGER.debug(
|
|
477
|
-
"%s: Connection refused on telnet connect", self.host,
|
|
537
|
+
"%s: Connection refused on telnet connect: %s", self.host, err
|
|
478
538
|
)
|
|
479
539
|
raise AvrNetworkError(
|
|
480
540
|
f"ConnectionRefusedError: {err}", "telnet connect"
|
|
481
541
|
) from err
|
|
482
542
|
except (OSError, IOError) as err:
|
|
483
543
|
_LOGGER.debug(
|
|
484
|
-
"%s: Connection failed on telnet reconnect", self.host,
|
|
544
|
+
"%s: Connection failed on telnet reconnect: %s", self.host, err
|
|
485
545
|
)
|
|
486
546
|
raise AvrNetworkError(f"OSError: {err}", "telnet connect") from err
|
|
487
|
-
_LOGGER.debug("%s: telnet connection complete", self.host)
|
|
488
547
|
self._protocol = cast(DenonAVRTelnetProtocol, transport_protocol[1])
|
|
548
|
+
_LOGGER.debug("%s: telnet connection established", self.host)
|
|
489
549
|
self._connection_enabled = True
|
|
490
550
|
self._last_message_time = time.monotonic()
|
|
491
551
|
self._schedule_monitor()
|
|
@@ -507,6 +567,7 @@ class DenonAVRTelnetApi:
|
|
|
507
567
|
"PSREFLEV ?",
|
|
508
568
|
"PSDYNVOL ?",
|
|
509
569
|
"MS?",
|
|
570
|
+
skip_confirmation=True,
|
|
510
571
|
)
|
|
511
572
|
|
|
512
573
|
def _schedule_monitor(self) -> None:
|
|
@@ -527,8 +588,6 @@ class DenonAVRTelnetApi:
|
|
|
527
588
|
_LOGGER.info(
|
|
528
589
|
"%s: Keep alive failed, disconnecting and reconnecting", self.host
|
|
529
590
|
)
|
|
530
|
-
if self._protocol is not None:
|
|
531
|
-
self._protocol.close()
|
|
532
591
|
self._handle_disconnected()
|
|
533
592
|
return
|
|
534
593
|
|
|
@@ -540,16 +599,20 @@ class DenonAVRTelnetApi:
|
|
|
540
599
|
|
|
541
600
|
def _handle_disconnected(self) -> None:
|
|
542
601
|
"""Handle disconnected."""
|
|
543
|
-
_LOGGER.debug("%s: disconnected", self.host)
|
|
544
|
-
self._protocol
|
|
602
|
+
_LOGGER.debug("%s: handle disconnected", self.host)
|
|
603
|
+
if self._protocol is not None:
|
|
604
|
+
self._protocol.close()
|
|
605
|
+
self._protocol = None
|
|
545
606
|
self._stop_monitor()
|
|
546
607
|
if not self._connection_enabled:
|
|
547
608
|
return
|
|
548
|
-
self._reconnect_task
|
|
609
|
+
if self._reconnect_task is None:
|
|
610
|
+
self._reconnect_task = asyncio.create_task(self._async_reconnect())
|
|
549
611
|
|
|
550
612
|
async def async_disconnect(self) -> None:
|
|
551
613
|
"""Close the connection to the receiver asynchronously."""
|
|
552
614
|
async with self._connect_lock:
|
|
615
|
+
_LOGGER.debug("%s: telnet disconnecting", self.host)
|
|
553
616
|
self._connection_enabled = False
|
|
554
617
|
self._stop_monitor()
|
|
555
618
|
reconnect_task = self._reconnect_task
|
|
@@ -565,6 +628,7 @@ class DenonAVRTelnetApi:
|
|
|
565
628
|
await reconnect_task
|
|
566
629
|
except asyncio.CancelledError:
|
|
567
630
|
pass
|
|
631
|
+
_LOGGER.debug("%s: telnet disconnected", self.host)
|
|
568
632
|
|
|
569
633
|
async def _async_reconnect(self) -> None:
|
|
570
634
|
"""Reconnect to the receiver asynchronously."""
|
|
@@ -572,27 +636,36 @@ class DenonAVRTelnetApi:
|
|
|
572
636
|
|
|
573
637
|
while self._connection_enabled and not self.healthy:
|
|
574
638
|
async with self._connect_lock:
|
|
639
|
+
_LOGGER.debug("%s: Telnet reconnecting", self.host)
|
|
575
640
|
try:
|
|
576
641
|
await self._async_establish_connection()
|
|
577
642
|
except AvrTimoutError:
|
|
578
643
|
_LOGGER.debug(
|
|
579
644
|
"%s: Timeout exception on telnet reconnect", self.host
|
|
580
645
|
)
|
|
581
|
-
except AvrNetworkError as
|
|
582
|
-
_LOGGER.debug("%s: %s", self.host,
|
|
583
|
-
except
|
|
646
|
+
except AvrNetworkError as err:
|
|
647
|
+
_LOGGER.debug("%s: %s", self.host, err)
|
|
648
|
+
except AvrProcessingError as err:
|
|
649
|
+
_LOGGER.debug(
|
|
650
|
+
"%s: Failed updating state on telnet reconnect: %s",
|
|
651
|
+
self.host,
|
|
652
|
+
err,
|
|
653
|
+
)
|
|
654
|
+
except Exception as err: # pylint: disable=broad-except
|
|
584
655
|
_LOGGER.error(
|
|
585
656
|
"%s: Unexpected exception on telnet reconnect",
|
|
586
657
|
self.host,
|
|
587
|
-
exc_info=
|
|
658
|
+
exc_info=err,
|
|
588
659
|
)
|
|
589
660
|
else:
|
|
590
661
|
_LOGGER.info("%s: Telnet reconnected", self.host)
|
|
591
|
-
|
|
662
|
+
break
|
|
592
663
|
|
|
593
664
|
await asyncio.sleep(backoff)
|
|
594
665
|
backoff = min(30.0, backoff * 2)
|
|
595
666
|
|
|
667
|
+
self._reconnect_task = None
|
|
668
|
+
|
|
596
669
|
def register_callback(
|
|
597
670
|
self, event: str, callback: Callable[[str, str, str], Awaitable[None]]
|
|
598
671
|
) -> None:
|
|
@@ -603,6 +676,8 @@ class DenonAVRTelnetApi:
|
|
|
603
676
|
|
|
604
677
|
if event not in self._callbacks.keys():
|
|
605
678
|
self._callbacks[event] = []
|
|
679
|
+
elif callback in self._callbacks[event]:
|
|
680
|
+
return
|
|
606
681
|
self._callbacks[event].append(callback)
|
|
607
682
|
|
|
608
683
|
def unregister_callback(
|
|
@@ -617,6 +692,8 @@ class DenonAVRTelnetApi:
|
|
|
617
692
|
self, callback: Callable[[str], Awaitable[None]]
|
|
618
693
|
) -> None:
|
|
619
694
|
"""Register a callback handler for raw telnet messages."""
|
|
695
|
+
if callback in self._raw_callbacks:
|
|
696
|
+
return
|
|
620
697
|
self._raw_callbacks.append(callback)
|
|
621
698
|
|
|
622
699
|
def _unregister_raw_callback(
|
|
@@ -682,7 +759,7 @@ class DenonAVRTelnetApi:
|
|
|
682
759
|
# We don't want a single bad callback to trip up the
|
|
683
760
|
# whole system and prevent further execution
|
|
684
761
|
_LOGGER.error(
|
|
685
|
-
"%s: Raw callback caused an unhandled exception %s",
|
|
762
|
+
"%s: Raw callback caused an unhandled exception: %s",
|
|
686
763
|
self.host,
|
|
687
764
|
err,
|
|
688
765
|
)
|
|
@@ -695,7 +772,7 @@ class DenonAVRTelnetApi:
|
|
|
695
772
|
# We don't want a single bad callback to trip up the
|
|
696
773
|
# whole system and prevent further execution
|
|
697
774
|
_LOGGER.error(
|
|
698
|
-
"%s: Event callback caused an unhandled exception %s",
|
|
775
|
+
"%s: Event callback caused an unhandled exception: %s",
|
|
699
776
|
self.host,
|
|
700
777
|
err,
|
|
701
778
|
)
|
|
@@ -708,7 +785,7 @@ class DenonAVRTelnetApi:
|
|
|
708
785
|
# We don't want a single bad callback to trip up the
|
|
709
786
|
# whole system and prevent further execution
|
|
710
787
|
_LOGGER.error(
|
|
711
|
-
"%s: Event callback caused an unhandled exception %s",
|
|
788
|
+
"%s: Event callback caused an unhandled exception: %s",
|
|
712
789
|
self.host,
|
|
713
790
|
err,
|
|
714
791
|
)
|
|
@@ -731,35 +808,45 @@ class DenonAVRTelnetApi:
|
|
|
731
808
|
self._send_confirmation_event.set()
|
|
732
809
|
_LOGGER.debug("Command %s confirmed", command)
|
|
733
810
|
|
|
734
|
-
async def _async_send_command(
|
|
811
|
+
async def _async_send_command(
|
|
812
|
+
self, command: str, skip_confirmation: bool = False
|
|
813
|
+
) -> None:
|
|
735
814
|
"""Send one telnet command to the receiver."""
|
|
736
815
|
async with self._send_lock:
|
|
737
|
-
|
|
738
|
-
|
|
816
|
+
if not skip_confirmation:
|
|
817
|
+
self._send_confirmation_command = command
|
|
818
|
+
self._send_confirmation_event.clear()
|
|
739
819
|
if not self.connected or not self.healthy:
|
|
740
820
|
raise AvrProcessingError(
|
|
741
821
|
f"Error sending command {command}. Telnet connected: "
|
|
742
822
|
f"{self.connected}, Connection healthy: {self.healthy}"
|
|
743
823
|
)
|
|
744
824
|
self._protocol.write(f"{command}\r")
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
825
|
+
if not skip_confirmation:
|
|
826
|
+
try:
|
|
827
|
+
await asyncio.wait_for(
|
|
828
|
+
self._send_confirmation_event.wait(),
|
|
829
|
+
self._send_confirmation_timeout,
|
|
830
|
+
)
|
|
831
|
+
except asyncio.TimeoutError:
|
|
832
|
+
_LOGGER.info(
|
|
833
|
+
"Timeout waiting for confirmation of command: %s", command
|
|
834
|
+
)
|
|
835
|
+
finally:
|
|
836
|
+
self._send_confirmation_command = ""
|
|
754
837
|
|
|
755
|
-
async def async_send_commands(
|
|
838
|
+
async def async_send_commands(
|
|
839
|
+
self, *commands: str, skip_confirmation: bool = False
|
|
840
|
+
) -> None:
|
|
756
841
|
"""Send telnet commands to the receiver."""
|
|
757
842
|
for command in commands:
|
|
758
|
-
await self._async_send_command(command)
|
|
843
|
+
await self._async_send_command(command, skip_confirmation=skip_confirmation)
|
|
759
844
|
|
|
760
|
-
def send_commands(self, *commands: str) -> None:
|
|
845
|
+
def send_commands(self, *commands: str, skip_confirmation: bool = False) -> None:
|
|
761
846
|
"""Send telnet commands to the receiver."""
|
|
762
|
-
task = asyncio.create_task(
|
|
847
|
+
task = asyncio.create_task(
|
|
848
|
+
self.async_send_commands(*commands, skip_confirmation=skip_confirmation)
|
|
849
|
+
)
|
|
763
850
|
self._send_tasks.add(task)
|
|
764
851
|
task.add_done_callback(self._send_tasks.discard)
|
|
765
852
|
|
denonavr/audyssey.py
CHANGED
|
@@ -99,12 +99,14 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
99
99
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
100
100
|
) -> None:
|
|
101
101
|
"""Update Audyssey asynchronously."""
|
|
102
|
+
_LOGGER.debug("Starting Audyssey update")
|
|
102
103
|
# Ensure instance is setup before updating
|
|
103
104
|
if not self._is_setup:
|
|
104
105
|
self.setup()
|
|
105
106
|
|
|
106
107
|
# Update state
|
|
107
108
|
await self.async_update_audyssey(global_update=global_update, cache_id=cache_id)
|
|
109
|
+
_LOGGER.debug("Finished Audyssey update")
|
|
108
110
|
|
|
109
111
|
async def async_update_audyssey(
|
|
110
112
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
@@ -117,13 +119,16 @@ class DenonAVRAudyssey(DenonAVRFoundation):
|
|
|
117
119
|
|
|
118
120
|
# Audyssey is only available for avr 2016 update
|
|
119
121
|
if self._device.use_avr_2016_update:
|
|
120
|
-
|
|
121
|
-
self.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
try:
|
|
123
|
+
await self.async_update_attrs_appcommand(
|
|
124
|
+
self.appcommand0300_attrs,
|
|
125
|
+
appcommand0300=True,
|
|
126
|
+
global_update=global_update,
|
|
127
|
+
cache_id=cache_id,
|
|
128
|
+
)
|
|
129
|
+
except AvrProcessingError as err:
|
|
130
|
+
# Don't raise an error here, because not all devices support it
|
|
131
|
+
_LOGGER.debug("Updating Audyssey failed: %s", err)
|
|
127
132
|
|
|
128
133
|
async def _async_set_audyssey(self, cmd: AppCommandCmd) -> None:
|
|
129
134
|
"""Set Audyssey parameter."""
|