denonavr 1.0.0__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 -1
- denonavr/api.py +136 -60
- denonavr/const.py +1 -1
- denonavr/decorators.py +5 -32
- denonavr/denonavr.py +1 -1
- denonavr/foundation.py +6 -7
- denonavr/input.py +44 -3
- {denonavr-1.0.0.dist-info → denonavr-1.0.1.dist-info}/METADATA +17 -17
- denonavr-1.0.1.dist-info/RECORD +19 -0
- {denonavr-1.0.0.dist-info → denonavr-1.0.1.dist-info}/WHEEL +1 -1
- denonavr-1.0.0.dist-info/RECORD +0 -19
- {denonavr-1.0.0.dist-info → denonavr-1.0.1.dist-info}/LICENSE +0 -0
- {denonavr-1.0.0.dist-info → denonavr-1.0.1.dist-info}/top_level.txt +0 -0
denonavr/__init__.py
CHANGED
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."""
|
|
@@ -481,8 +544,8 @@ class DenonAVRTelnetApi:
|
|
|
481
544
|
"%s: Connection failed on telnet reconnect: %s", self.host, err
|
|
482
545
|
)
|
|
483
546
|
raise AvrNetworkError(f"OSError: {err}", "telnet connect") from err
|
|
484
|
-
_LOGGER.debug("%s: telnet connection established", self.host)
|
|
485
547
|
self._protocol = cast(DenonAVRTelnetProtocol, transport_protocol[1])
|
|
548
|
+
_LOGGER.debug("%s: telnet connection established", self.host)
|
|
486
549
|
self._connection_enabled = True
|
|
487
550
|
self._last_message_time = time.monotonic()
|
|
488
551
|
self._schedule_monitor()
|
|
@@ -543,7 +606,8 @@ class DenonAVRTelnetApi:
|
|
|
543
606
|
self._stop_monitor()
|
|
544
607
|
if not self._connection_enabled:
|
|
545
608
|
return
|
|
546
|
-
self._reconnect_task
|
|
609
|
+
if self._reconnect_task is None:
|
|
610
|
+
self._reconnect_task = asyncio.create_task(self._async_reconnect())
|
|
547
611
|
|
|
548
612
|
async def async_disconnect(self) -> None:
|
|
549
613
|
"""Close the connection to the receiver asynchronously."""
|
|
@@ -581,6 +645,12 @@ class DenonAVRTelnetApi:
|
|
|
581
645
|
)
|
|
582
646
|
except AvrNetworkError as err:
|
|
583
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
|
+
)
|
|
584
654
|
except Exception as err: # pylint: disable=broad-except
|
|
585
655
|
_LOGGER.error(
|
|
586
656
|
"%s: Unexpected exception on telnet reconnect",
|
|
@@ -589,11 +659,13 @@ class DenonAVRTelnetApi:
|
|
|
589
659
|
)
|
|
590
660
|
else:
|
|
591
661
|
_LOGGER.info("%s: Telnet reconnected", self.host)
|
|
592
|
-
|
|
662
|
+
break
|
|
593
663
|
|
|
594
664
|
await asyncio.sleep(backoff)
|
|
595
665
|
backoff = min(30.0, backoff * 2)
|
|
596
666
|
|
|
667
|
+
self._reconnect_task = None
|
|
668
|
+
|
|
597
669
|
def register_callback(
|
|
598
670
|
self, event: str, callback: Callable[[str, str, str], Awaitable[None]]
|
|
599
671
|
) -> None:
|
|
@@ -604,6 +676,8 @@ class DenonAVRTelnetApi:
|
|
|
604
676
|
|
|
605
677
|
if event not in self._callbacks.keys():
|
|
606
678
|
self._callbacks[event] = []
|
|
679
|
+
elif callback in self._callbacks[event]:
|
|
680
|
+
return
|
|
607
681
|
self._callbacks[event].append(callback)
|
|
608
682
|
|
|
609
683
|
def unregister_callback(
|
|
@@ -618,6 +692,8 @@ class DenonAVRTelnetApi:
|
|
|
618
692
|
self, callback: Callable[[str], Awaitable[None]]
|
|
619
693
|
) -> None:
|
|
620
694
|
"""Register a callback handler for raw telnet messages."""
|
|
695
|
+
if callback in self._raw_callbacks:
|
|
696
|
+
return
|
|
621
697
|
self._raw_callbacks.append(callback)
|
|
622
698
|
|
|
623
699
|
def _unregister_raw_callback(
|
denonavr/const.py
CHANGED
|
@@ -274,7 +274,6 @@ SOUND_MODE_MAPPING = {
|
|
|
274
274
|
"MULTI IN + DOLBY SURROUND",
|
|
275
275
|
"MULTI IN + DSUR",
|
|
276
276
|
"MULTI IN + NEURAL:X",
|
|
277
|
-
"NEURAL:X",
|
|
278
277
|
"NEURAL",
|
|
279
278
|
"STANDARD(DOLBY)",
|
|
280
279
|
],
|
|
@@ -294,6 +293,7 @@ SOUND_MODE_MAPPING = {
|
|
|
294
293
|
"DTS:X",
|
|
295
294
|
"M CH IN+DSUR",
|
|
296
295
|
"MULTI CH IN",
|
|
296
|
+
"NEURAL:X",
|
|
297
297
|
"STANDARD(DTS)",
|
|
298
298
|
"VIRTUAL:X",
|
|
299
299
|
],
|
denonavr/decorators.py
CHANGED
|
@@ -10,14 +10,11 @@ This module implements the REST API to Denon AVR receivers.
|
|
|
10
10
|
import inspect
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
13
|
-
import xml.etree.ElementTree as ET
|
|
14
13
|
from functools import wraps
|
|
15
14
|
from typing import Callable, TypeVar
|
|
16
15
|
|
|
17
16
|
import httpx
|
|
18
17
|
from asyncstdlib import lru_cache
|
|
19
|
-
from defusedxml import DefusedXmlException
|
|
20
|
-
from defusedxml.ElementTree import ParseError
|
|
21
18
|
|
|
22
19
|
from .exceptions import (
|
|
23
20
|
AvrForbiddenError,
|
|
@@ -33,12 +30,7 @@ AnyT = TypeVar("AnyT")
|
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
def async_handle_receiver_exceptions(func: Callable[..., AnyT]) -> Callable[..., AnyT]:
|
|
36
|
-
"""
|
|
37
|
-
Handle exceptions raised when calling a Denon AVR endpoint asynchronously.
|
|
38
|
-
|
|
39
|
-
The decorated function must either have a string variable as second
|
|
40
|
-
argument or as "request" keyword argument.
|
|
41
|
-
"""
|
|
33
|
+
"""Handle exceptions raised when calling a Denon AVR endpoint asynchronously."""
|
|
42
34
|
|
|
43
35
|
@wraps(func)
|
|
44
36
|
async def wrapper(*args, **kwargs):
|
|
@@ -64,48 +56,29 @@ def async_handle_receiver_exceptions(func: Callable[..., AnyT]) -> Callable[...,
|
|
|
64
56
|
raise AvrInvalidResponseError(
|
|
65
57
|
f"RemoteProtocolError: {err}", err.request
|
|
66
58
|
) from err
|
|
67
|
-
except (
|
|
68
|
-
ET.ParseError,
|
|
69
|
-
DefusedXmlException,
|
|
70
|
-
ParseError,
|
|
71
|
-
UnicodeDecodeError,
|
|
72
|
-
) as err:
|
|
73
|
-
_LOGGER.debug(
|
|
74
|
-
"Defusedxml parse error on request %s: %s", (args, kwargs), err
|
|
75
|
-
)
|
|
76
|
-
raise AvrInvalidResponseError(
|
|
77
|
-
f"XMLParseError: {err}", (args, kwargs)
|
|
78
|
-
) from err
|
|
79
59
|
|
|
80
60
|
return wrapper
|
|
81
61
|
|
|
82
62
|
|
|
83
63
|
def cache_result(func: Callable[..., AnyT]) -> Callable[..., AnyT]:
|
|
84
64
|
"""
|
|
85
|
-
Decorate a function to cache its results with an lru_cache of maxsize
|
|
65
|
+
Decorate a function to cache its results with an lru_cache of maxsize 32.
|
|
86
66
|
|
|
87
67
|
This decorator also sets an "cache_id" keyword argument if it is not set yet.
|
|
88
|
-
When an exception occurs it clears lru_cache to prevent memory leaks in
|
|
89
|
-
home-assistant when receiver instances are created and deleted right
|
|
90
|
-
away in case the device is offline on setup.
|
|
91
68
|
"""
|
|
92
69
|
if inspect.signature(func).parameters.get("cache_id") is None:
|
|
93
70
|
raise AttributeError(
|
|
94
71
|
f"Function {func} does not have a 'cache_id' keyword parameter"
|
|
95
72
|
)
|
|
96
73
|
|
|
97
|
-
lru_decorator = lru_cache(maxsize=
|
|
74
|
+
lru_decorator = lru_cache(maxsize=32)
|
|
98
75
|
cached_func = lru_decorator(func)
|
|
99
76
|
|
|
100
77
|
@wraps(func)
|
|
101
78
|
async def wrapper(*args, **kwargs):
|
|
102
79
|
if kwargs.get("cache_id") is None:
|
|
103
80
|
kwargs["cache_id"] = time.time()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
except Exception as err:
|
|
107
|
-
_LOGGER.debug("Exception raised, clearing cache: %s", err)
|
|
108
|
-
cached_func.cache_clear()
|
|
109
|
-
raise
|
|
81
|
+
|
|
82
|
+
return await cached_func(*args, **kwargs)
|
|
110
83
|
|
|
111
84
|
return wrapper
|
denonavr/denonavr.py
CHANGED
|
@@ -519,7 +519,7 @@ class DenonAVR(DenonAVRFoundation):
|
|
|
519
519
|
"""
|
|
520
520
|
if not callable(async_client_getter):
|
|
521
521
|
raise AvrCommandError("Provided object is not callable")
|
|
522
|
-
self._device.api.
|
|
522
|
+
self._device.api.httpx_async_client.client_getter = async_client_getter
|
|
523
523
|
|
|
524
524
|
async def async_dynamic_eq_off(self) -> None:
|
|
525
525
|
"""Turn DynamicEQ off."""
|
denonavr/foundation.py
CHANGED
|
@@ -15,7 +15,6 @@ from copy import deepcopy
|
|
|
15
15
|
from typing import Dict, List, Optional, Union
|
|
16
16
|
|
|
17
17
|
import attr
|
|
18
|
-
import httpx
|
|
19
18
|
|
|
20
19
|
from .api import DenonAVRApi, DenonAVRTelnetApi
|
|
21
20
|
from .appcommand import AppCommandCmd, AppCommands
|
|
@@ -146,15 +145,15 @@ class DenonAVRDeviceInfo:
|
|
|
146
145
|
_LOGGER.debug("Starting device setup")
|
|
147
146
|
# Reduce read timeout during receiver identification
|
|
148
147
|
# deviceinfo endpoint takes very long to return 404
|
|
149
|
-
|
|
150
|
-
self.api.
|
|
148
|
+
read_timeout = self.api.read_timeout
|
|
149
|
+
self.api.read_timeout = self.api.timeout
|
|
151
150
|
try:
|
|
152
151
|
_LOGGER.debug("Identifying receiver")
|
|
153
152
|
await self.async_identify_receiver()
|
|
154
153
|
_LOGGER.debug("Getting device info")
|
|
155
154
|
await self.async_get_device_info()
|
|
156
155
|
finally:
|
|
157
|
-
self.api.
|
|
156
|
+
self.api.read_timeout = read_timeout
|
|
158
157
|
_LOGGER.debug("Identifying update method")
|
|
159
158
|
await self.async_identify_update_method()
|
|
160
159
|
|
|
@@ -323,7 +322,7 @@ class DenonAVRDeviceInfo:
|
|
|
323
322
|
self._set_friendly_name(xml)
|
|
324
323
|
|
|
325
324
|
async def async_verify_avr_2016_update_method(
|
|
326
|
-
self, cache_id: Hashable = None
|
|
325
|
+
self, *, cache_id: Hashable = None
|
|
327
326
|
) -> None:
|
|
328
327
|
"""Verify if avr 2016 update method is working."""
|
|
329
328
|
# Nothing to do if Appcommand.xml interface is not supported
|
|
@@ -833,9 +832,9 @@ def set_api_timeout(
|
|
|
833
832
|
) -> float:
|
|
834
833
|
"""Change API timeout on timeout changes too."""
|
|
835
834
|
# First change _device.api.host then return value
|
|
836
|
-
timeout = httpx.Timeout(value, read=max(value, 15.0))
|
|
837
835
|
# pylint: disable=protected-access
|
|
838
|
-
instance._device.api.timeout =
|
|
836
|
+
instance._device.api.timeout = value
|
|
837
|
+
instance._device.api.read_timeout = max(value, 15.0)
|
|
839
838
|
instance._device.telnet_api.timeout = value
|
|
840
839
|
return value
|
|
841
840
|
|
denonavr/input.py
CHANGED
|
@@ -168,6 +168,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
168
168
|
converter=attr.converters.optional(str), default=None
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
+
_renamed_sources_warnings: Set[Tuple[str, str]] = attr.ib(
|
|
172
|
+
validator=attr.validators.deep_iterable(
|
|
173
|
+
attr.validators.instance_of(tuple), attr.validators.instance_of(set)
|
|
174
|
+
),
|
|
175
|
+
default=attr.Factory(set),
|
|
176
|
+
)
|
|
177
|
+
|
|
171
178
|
# Update tags for attributes
|
|
172
179
|
# AppCommand.xml interface
|
|
173
180
|
appcommand_attrs = {AppCommands.GetAllZoneSource: None}
|
|
@@ -516,6 +523,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
516
523
|
except AttributeError:
|
|
517
524
|
continue
|
|
518
525
|
|
|
526
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
527
|
+
|
|
519
528
|
return (renamed_sources, deleted_sources)
|
|
520
529
|
|
|
521
530
|
async def async_get_changed_sources_status_xml(
|
|
@@ -604,6 +613,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
604
613
|
except IndexError:
|
|
605
614
|
_LOGGER.error("List of deleted sources incomplete, continuing anyway")
|
|
606
615
|
|
|
616
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
617
|
+
|
|
607
618
|
return (renamed_sources, deleted_sources)
|
|
608
619
|
|
|
609
620
|
async def async_update_inputfuncs(
|
|
@@ -859,10 +870,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
859
870
|
async def _async_test_image_accessible(self) -> None:
|
|
860
871
|
"""Test if image URL is accessible."""
|
|
861
872
|
if self._image_available is None and self._image_url is not None:
|
|
862
|
-
client = self._device.api.
|
|
873
|
+
client = self._device.api.httpx_async_client.client_getter()
|
|
863
874
|
try:
|
|
864
875
|
res = await client.get(
|
|
865
|
-
self._image_url,
|
|
876
|
+
self._image_url,
|
|
877
|
+
timeout=httpx.Timeout(
|
|
878
|
+
self._device.api.timeout, read=self._device.api.read_timeout
|
|
879
|
+
),
|
|
866
880
|
)
|
|
867
881
|
res.raise_for_status()
|
|
868
882
|
except httpx.TimeoutException:
|
|
@@ -878,7 +892,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
878
892
|
self._image_available = True
|
|
879
893
|
finally:
|
|
880
894
|
# Close the default AsyncClient but keep custom clients open
|
|
881
|
-
if self._device.api.is_default_async_client():
|
|
895
|
+
if self._device.api.httpx_async_client.is_default_async_client():
|
|
882
896
|
await client.aclose()
|
|
883
897
|
# Already tested that image URL is not accessible
|
|
884
898
|
elif not self._image_available:
|
|
@@ -894,6 +908,33 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
894
908
|
self._station = None
|
|
895
909
|
self._image_url = None
|
|
896
910
|
|
|
911
|
+
def _replace_duplicate_sources(self, sources: Dict[str, str]) -> None:
|
|
912
|
+
"""Replace duplicate renamed sources (values) with their original names."""
|
|
913
|
+
seen_values = set()
|
|
914
|
+
duplicate_values = set()
|
|
915
|
+
|
|
916
|
+
for value in sources.values():
|
|
917
|
+
if value in seen_values:
|
|
918
|
+
duplicate_values.add(value)
|
|
919
|
+
seen_values.add(value)
|
|
920
|
+
|
|
921
|
+
for duplicate in duplicate_values:
|
|
922
|
+
for key, value in sources.items():
|
|
923
|
+
if value == duplicate:
|
|
924
|
+
sources[key] = key
|
|
925
|
+
|
|
926
|
+
if (key, value) not in self._renamed_sources_warnings:
|
|
927
|
+
_LOGGER.warning(
|
|
928
|
+
(
|
|
929
|
+
"Input source '%s' is renamed to non-unique name '%s'. "
|
|
930
|
+
"Using original name. Please choose unique names in "
|
|
931
|
+
"your receiver's web-interface"
|
|
932
|
+
),
|
|
933
|
+
key,
|
|
934
|
+
value,
|
|
935
|
+
)
|
|
936
|
+
self._renamed_sources_warnings.add((key, value))
|
|
937
|
+
|
|
897
938
|
##############
|
|
898
939
|
# Properties #
|
|
899
940
|
##############
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: denonavr
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Automation Library for Denon AVR receivers
|
|
5
5
|
Author-email: Oliver Goetz <scarface@mywoh.de>
|
|
6
6
|
License: MIT
|
|
@@ -24,23 +24,23 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
24
24
|
Requires-Python: >=3.7
|
|
25
25
|
Description-Content-Type: text/markdown; charset=UTF-8
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: asyncstdlib
|
|
28
|
-
Requires-Dist: attrs
|
|
29
|
-
Requires-Dist: defusedxml
|
|
30
|
-
Requires-Dist: ftfy
|
|
31
|
-
Requires-Dist: httpx
|
|
32
|
-
Requires-Dist: netifaces
|
|
33
|
-
Requires-Dist: async-timeout
|
|
27
|
+
Requires-Dist: asyncstdlib>=3.10.2
|
|
28
|
+
Requires-Dist: attrs>=21.2.0
|
|
29
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
30
|
+
Requires-Dist: ftfy>=6.1.1
|
|
31
|
+
Requires-Dist: httpx>=0.23.1
|
|
32
|
+
Requires-Dist: netifaces>=0.11.0
|
|
33
|
+
Requires-Dist: async-timeout>=4.0.2; python_version < "3.11"
|
|
34
34
|
Provides-Extra: testing
|
|
35
|
-
Requires-Dist: pydocstyle
|
|
36
|
-
Requires-Dist: pylint
|
|
37
|
-
Requires-Dist: pytest
|
|
38
|
-
Requires-Dist: pytest-cov
|
|
39
|
-
Requires-Dist: pytest-timeout
|
|
40
|
-
Requires-Dist: pytest-asyncio
|
|
41
|
-
Requires-Dist: pytest-httpx
|
|
42
|
-
Requires-Dist: flake8-docstrings
|
|
43
|
-
Requires-Dist: flake8
|
|
35
|
+
Requires-Dist: pydocstyle; extra == "testing"
|
|
36
|
+
Requires-Dist: pylint; extra == "testing"
|
|
37
|
+
Requires-Dist: pytest; extra == "testing"
|
|
38
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
39
|
+
Requires-Dist: pytest-timeout; extra == "testing"
|
|
40
|
+
Requires-Dist: pytest-asyncio; extra == "testing"
|
|
41
|
+
Requires-Dist: pytest-httpx; extra == "testing"
|
|
42
|
+
Requires-Dist: flake8-docstrings; extra == "testing"
|
|
43
|
+
Requires-Dist: flake8; extra == "testing"
|
|
44
44
|
|
|
45
45
|
# denonavr
|
|
46
46
|
[](https://github.com/ol-iver/denonavr/releases/latest)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
denonavr/__init__.py,sha256=2EHaA5L852gUHTxouwZk4aLyww3HNlkgTPS5CzYkn4w,1325
|
|
2
|
+
denonavr/api.py,sha256=0i68EOx6hhSqGeQaIVw9bpbK7fxLKB_ynR_FtStA2J0,31462
|
|
3
|
+
denonavr/appcommand.py,sha256=H37VK4n-yW0ISvR1hfX8G4udQtGASoY44QNaXtLSidE,7784
|
|
4
|
+
denonavr/audyssey.py,sha256=bHzgFRdUwLQozP2Or59jn6qfYU4WSQxiGKixBx7xxQo,11085
|
|
5
|
+
denonavr/const.py,sha256=HoF8Zn03rC5oiAzl_y4YbYeft6tCKUU95z_GG-sPTMY,21464
|
|
6
|
+
denonavr/decorators.py,sha256=tLoVjGtDmN75fhwCgNREMGjbB6varDERnYlrhC6yIZ8,2733
|
|
7
|
+
denonavr/denonavr.py,sha256=twI100XNXvR6cfqtYKiegjhapOlKBUUSeZFdNBgjUew,24013
|
|
8
|
+
denonavr/exceptions.py,sha256=naP7MCuNH98rv7y_lDdO7ayyvQGywnsfpHhW-o9tKeo,1725
|
|
9
|
+
denonavr/foundation.py,sha256=c8drYLPThQS2cXs6RJbhLMpcb097AA2rC4ofd9fJobA,31432
|
|
10
|
+
denonavr/input.py,sha256=kbKPePkdFHQSHKzOvAYudeSyJiRrUh0PxIOkG38kDUo,43050
|
|
11
|
+
denonavr/soundmode.py,sha256=MKyBtSQ-1gtnNyT_qQhbGDlPQuNS3QzBurjUUWNPKeo,12464
|
|
12
|
+
denonavr/ssdp.py,sha256=4gyvN0yo9qSnJ7l6iJmSC_5qW45Fq2Tt2RACpxFMSqg,7839
|
|
13
|
+
denonavr/tonecontrol.py,sha256=DfHzziEeLdYiyTbJKcGk5cCPTvKBgjMqQReYp0qoF0U,12474
|
|
14
|
+
denonavr/volume.py,sha256=S3UeyassnErOSqiaS48Q4Vo9QHJTyG1jnyEDSRFOfdw,7341
|
|
15
|
+
denonavr-1.0.1.dist-info/LICENSE,sha256=hcKXAoZnRee1NRWnxTpKyjKCz4aHhw76sZUZoB3qPTw,1068
|
|
16
|
+
denonavr-1.0.1.dist-info/METADATA,sha256=CNw5cVpYvc3owjqaBB0Q3beEDgDB4sohaqD9A9nnDck,5233
|
|
17
|
+
denonavr-1.0.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
18
|
+
denonavr-1.0.1.dist-info/top_level.txt,sha256=GKwe6bOaw_R68BR7x-C7VD3bDFKKNBf0pRkWywUZWIs,9
|
|
19
|
+
denonavr-1.0.1.dist-info/RECORD,,
|
denonavr-1.0.0.dist-info/RECORD
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
denonavr/__init__.py,sha256=jkmT2PjmR9ncL4gwegkmN0UMpyA7a2OQn7nU9nbqbZk,1325
|
|
2
|
-
denonavr/api.py,sha256=IL8KMUj6_S_9sFC_DnlmYV0shjfI4lYu7HO3Yb-WjdE,29167
|
|
3
|
-
denonavr/appcommand.py,sha256=H37VK4n-yW0ISvR1hfX8G4udQtGASoY44QNaXtLSidE,7784
|
|
4
|
-
denonavr/audyssey.py,sha256=bHzgFRdUwLQozP2Or59jn6qfYU4WSQxiGKixBx7xxQo,11085
|
|
5
|
-
denonavr/const.py,sha256=lbM3hKYWUd8cto3BUqplKvDpX_uvCr8-7KWWoyohkNE,21464
|
|
6
|
-
denonavr/decorators.py,sha256=yBAMcY3bFJs5DJFZ-7Zvln4_jjcjrux6js4lJoOM59I,3755
|
|
7
|
-
denonavr/denonavr.py,sha256=BUseoHsI4J-ApawMWW_63b61Nn9TasTR0rENiijRW0w,24000
|
|
8
|
-
denonavr/exceptions.py,sha256=naP7MCuNH98rv7y_lDdO7ayyvQGywnsfpHhW-o9tKeo,1725
|
|
9
|
-
denonavr/foundation.py,sha256=fUpOgMO9mgKSq8VW00QSZy75H-mfiIrrKrxqfTiBgfI,31443
|
|
10
|
-
denonavr/input.py,sha256=R9bMCUQYSU02SL3U-uC2bYBKzMd9nVLjRjM4JRvONbo,41408
|
|
11
|
-
denonavr/soundmode.py,sha256=MKyBtSQ-1gtnNyT_qQhbGDlPQuNS3QzBurjUUWNPKeo,12464
|
|
12
|
-
denonavr/ssdp.py,sha256=4gyvN0yo9qSnJ7l6iJmSC_5qW45Fq2Tt2RACpxFMSqg,7839
|
|
13
|
-
denonavr/tonecontrol.py,sha256=DfHzziEeLdYiyTbJKcGk5cCPTvKBgjMqQReYp0qoF0U,12474
|
|
14
|
-
denonavr/volume.py,sha256=S3UeyassnErOSqiaS48Q4Vo9QHJTyG1jnyEDSRFOfdw,7341
|
|
15
|
-
denonavr-1.0.0.dist-info/LICENSE,sha256=hcKXAoZnRee1NRWnxTpKyjKCz4aHhw76sZUZoB3qPTw,1068
|
|
16
|
-
denonavr-1.0.0.dist-info/METADATA,sha256=QsdEw_wHlMK5pthVZU2wP7bJu9eToruOQQgWpYkGFOw,5250
|
|
17
|
-
denonavr-1.0.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
18
|
-
denonavr-1.0.0.dist-info/top_level.txt,sha256=GKwe6bOaw_R68BR7x-C7VD3bDFKKNBf0pRkWywUZWIs,9
|
|
19
|
-
denonavr-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|