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/foundation.py
CHANGED
|
@@ -10,8 +10,9 @@ This module implements the foundation classes for Denon AVR receivers.
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
12
|
import xml.etree.ElementTree as ET
|
|
13
|
+
from collections.abc import Hashable
|
|
13
14
|
from copy import deepcopy
|
|
14
|
-
from typing import Dict,
|
|
15
|
+
from typing import Dict, List, Optional, Union
|
|
15
16
|
|
|
16
17
|
import attr
|
|
17
18
|
import httpx
|
|
@@ -25,19 +26,24 @@ from .const import (
|
|
|
25
26
|
AVR_X,
|
|
26
27
|
AVR_X_2016,
|
|
27
28
|
DENON_ATTR_SETATTR,
|
|
29
|
+
DENONAVR_TELNET_COMMANDS,
|
|
28
30
|
DENONAVR_URLS,
|
|
29
31
|
DESCRIPTION_TYPES,
|
|
30
32
|
DEVICEINFO_AVR_X_PATTERN,
|
|
31
33
|
DEVICEINFO_COMMAPI_PATTERN,
|
|
32
34
|
MAIN_ZONE,
|
|
35
|
+
POWER_STATES,
|
|
33
36
|
VALID_RECEIVER_TYPES,
|
|
34
37
|
VALID_ZONES,
|
|
35
38
|
ZONE2,
|
|
39
|
+
ZONE2_TELNET_COMMANDS,
|
|
36
40
|
ZONE2_URLS,
|
|
37
41
|
ZONE3,
|
|
42
|
+
ZONE3_TELNET_COMMANDS,
|
|
38
43
|
ZONE3_URLS,
|
|
39
44
|
ReceiverType,
|
|
40
45
|
ReceiverURLs,
|
|
46
|
+
TelnetCommands,
|
|
41
47
|
)
|
|
42
48
|
from .exceptions import (
|
|
43
49
|
AvrForbiddenError,
|
|
@@ -70,6 +76,9 @@ class DenonAVRDeviceInfo:
|
|
|
70
76
|
validator=attr.validators.optional(attr.validators.in_(VALID_RECEIVER_TYPES)),
|
|
71
77
|
default=None,
|
|
72
78
|
)
|
|
79
|
+
telnet_commands: TelnetCommands = attr.ib(
|
|
80
|
+
validator=attr.validators.instance_of(TelnetCommands), init=False
|
|
81
|
+
)
|
|
73
82
|
urls: ReceiverURLs = attr.ib(
|
|
74
83
|
validator=attr.validators.instance_of(ReceiverURLs), init=False
|
|
75
84
|
)
|
|
@@ -95,26 +104,29 @@ class DenonAVRDeviceInfo:
|
|
|
95
104
|
converter=attr.converters.optional(str), default=None
|
|
96
105
|
)
|
|
97
106
|
_is_setup: bool = attr.ib(converter=bool, default=False, init=False)
|
|
98
|
-
_allow_recovery: bool = attr.ib(converter=bool, default=
|
|
107
|
+
_allow_recovery: bool = attr.ib(converter=bool, default=True, init=True)
|
|
99
108
|
_setup_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
|
|
100
109
|
|
|
101
110
|
def __attrs_post_init__(self) -> None:
|
|
102
111
|
"""Initialize special attributes and callbacks."""
|
|
103
112
|
# URLs depending from value of self.zone attribute
|
|
104
113
|
if self.zone == MAIN_ZONE:
|
|
114
|
+
self.telnet_commands = DENONAVR_TELNET_COMMANDS
|
|
105
115
|
self.urls = DENONAVR_URLS
|
|
106
116
|
elif self.zone == ZONE2:
|
|
117
|
+
self.telnet_commands = ZONE2_TELNET_COMMANDS
|
|
107
118
|
self.urls = ZONE2_URLS
|
|
108
119
|
elif self.zone == ZONE3:
|
|
120
|
+
self.telnet_commands = ZONE3_TELNET_COMMANDS
|
|
109
121
|
self.urls = ZONE3_URLS
|
|
110
122
|
else:
|
|
111
|
-
raise ValueError("Invalid zone {
|
|
123
|
+
raise ValueError(f"Invalid zone {self.zone}")
|
|
112
124
|
|
|
113
125
|
async def _async_power_callback(
|
|
114
126
|
self, zone: str, event: str, parameter: str
|
|
115
127
|
) -> None:
|
|
116
128
|
"""Handle a power change event."""
|
|
117
|
-
if self.zone == zone:
|
|
129
|
+
if self.zone == zone and parameter in POWER_STATES:
|
|
118
130
|
self._power = parameter
|
|
119
131
|
|
|
120
132
|
def get_own_zone(self) -> str:
|
|
@@ -146,7 +158,12 @@ class DenonAVRDeviceInfo:
|
|
|
146
158
|
# Add tags for a potential AppCommand.xml update
|
|
147
159
|
self.api.add_appcommand_update_tag(AppCommands.GetAllZonePowerStatus)
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
power_event = "ZM"
|
|
162
|
+
if self.zone == ZONE2:
|
|
163
|
+
power_event = "Z2"
|
|
164
|
+
elif self.zone == ZONE3:
|
|
165
|
+
power_event = "Z3"
|
|
166
|
+
self.telnet_api.register_callback(power_event, self._async_power_callback)
|
|
150
167
|
|
|
151
168
|
self._is_setup = True
|
|
152
169
|
|
|
@@ -155,7 +172,7 @@ class DenonAVRDeviceInfo:
|
|
|
155
172
|
) -> None:
|
|
156
173
|
"""Update status asynchronously."""
|
|
157
174
|
# Ensure instance is setup before updating
|
|
158
|
-
if self._is_setup
|
|
175
|
+
if not self._is_setup:
|
|
159
176
|
await self.async_setup()
|
|
160
177
|
|
|
161
178
|
# Update power status
|
|
@@ -163,7 +180,7 @@ class DenonAVRDeviceInfo:
|
|
|
163
180
|
|
|
164
181
|
async def async_identify_receiver(self) -> None:
|
|
165
182
|
"""Identify receiver asynchronously."""
|
|
166
|
-
# Test Deviceinfo.xml if receiver is
|
|
183
|
+
# Test Deviceinfo.xml if receiver is an AVR-X with port 80 for pre 2016
|
|
167
184
|
# devices and port 8080 devices 2016 and later
|
|
168
185
|
# 2016 models has also some of the XML but not all, try first 2016
|
|
169
186
|
r_types = [AVR_X, AVR_X_2016]
|
|
@@ -184,15 +201,17 @@ class DenonAVRDeviceInfo:
|
|
|
184
201
|
exc_info=err,
|
|
185
202
|
)
|
|
186
203
|
|
|
187
|
-
# Raise error only when
|
|
204
|
+
# Raise error only when occurred at both types
|
|
188
205
|
timeout_errors += 1
|
|
189
206
|
if timeout_errors == len(r_types):
|
|
190
207
|
raise
|
|
191
208
|
|
|
192
209
|
except AvrRequestError as err:
|
|
193
210
|
_LOGGER.debug(
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
(
|
|
212
|
+
"Request error on port %s when identifying receiver, "
|
|
213
|
+
"device is not a %s receivers"
|
|
214
|
+
),
|
|
196
215
|
r_type.port,
|
|
197
216
|
r_type.type,
|
|
198
217
|
exc_info=err,
|
|
@@ -204,13 +223,13 @@ class DenonAVRDeviceInfo:
|
|
|
204
223
|
# Receiver identified, return
|
|
205
224
|
return
|
|
206
225
|
|
|
207
|
-
# If check of Deviceinfo.xml was not
|
|
226
|
+
# If check of Deviceinfo.xml was not successful, receiver is type AVR
|
|
208
227
|
self.receiver = AVR
|
|
209
228
|
self.api.port = AVR.port
|
|
210
229
|
|
|
211
230
|
@staticmethod
|
|
212
231
|
def _is_avr_x(deviceinfo: ET.Element) -> bool:
|
|
213
|
-
"""Evaluate Deviceinfo.xml if the device is
|
|
232
|
+
"""Evaluate Deviceinfo.xml if the device is an AVR-X device."""
|
|
214
233
|
# First test by CommApiVers
|
|
215
234
|
try:
|
|
216
235
|
if bool(
|
|
@@ -270,7 +289,7 @@ class DenonAVRDeviceInfo:
|
|
|
270
289
|
_LOGGER.info("AVR-X device, using AppCommand.xml interface")
|
|
271
290
|
self._set_friendly_name(xml)
|
|
272
291
|
|
|
273
|
-
if self.use_avr_2016_update
|
|
292
|
+
if not self.use_avr_2016_update:
|
|
274
293
|
try:
|
|
275
294
|
xml = await self.api.async_get_xml(self.urls.mainzone)
|
|
276
295
|
except (AvrTimoutError, AvrNetworkError) as err:
|
|
@@ -294,7 +313,7 @@ class DenonAVRDeviceInfo:
|
|
|
294
313
|
) -> None:
|
|
295
314
|
"""Verify if avr 2016 update method is working."""
|
|
296
315
|
# Nothing to do if Appcommand.xml interface is not supported
|
|
297
|
-
if self.
|
|
316
|
+
if self._is_setup and not self.use_avr_2016_update:
|
|
298
317
|
return
|
|
299
318
|
|
|
300
319
|
try:
|
|
@@ -306,7 +325,7 @@ class DenonAVRDeviceInfo:
|
|
|
306
325
|
except AvrForbiddenError:
|
|
307
326
|
# Recovery in case receiver changes port from 80 to 8080 which
|
|
308
327
|
# might happen at Denon AVR-X 2016 receivers
|
|
309
|
-
if self._allow_recovery
|
|
328
|
+
if self._allow_recovery:
|
|
310
329
|
self._allow_recovery = False
|
|
311
330
|
_LOGGER.warning(
|
|
312
331
|
"AppCommand.xml returns HTTP status 403. Running setup"
|
|
@@ -328,7 +347,7 @@ class DenonAVRDeviceInfo:
|
|
|
328
347
|
)
|
|
329
348
|
self.use_avr_2016_update = False
|
|
330
349
|
else:
|
|
331
|
-
if self._allow_recovery
|
|
350
|
+
if not self._allow_recovery:
|
|
332
351
|
_LOGGER.info("AppCommand.xml recovered from HTTP status 403 error")
|
|
333
352
|
self._allow_recovery = True
|
|
334
353
|
|
|
@@ -353,9 +372,7 @@ class DenonAVRDeviceInfo:
|
|
|
353
372
|
"""Get device information."""
|
|
354
373
|
port = DESCRIPTION_TYPES[self.receiver.type].port
|
|
355
374
|
command = DESCRIPTION_TYPES[self.receiver.type].url
|
|
356
|
-
url = "http://{host}:{port}{command}"
|
|
357
|
-
host=self.api.host, port=port, command=command
|
|
358
|
-
)
|
|
375
|
+
url = f"http://{self.api.host}:{port}{command}"
|
|
359
376
|
|
|
360
377
|
device_info = None
|
|
361
378
|
try:
|
|
@@ -368,8 +385,10 @@ class DenonAVRDeviceInfo:
|
|
|
368
385
|
raise
|
|
369
386
|
except AvrRequestError as err:
|
|
370
387
|
_LOGGER.error(
|
|
371
|
-
|
|
372
|
-
|
|
388
|
+
(
|
|
389
|
+
"During DenonAVR device identification, when trying to request"
|
|
390
|
+
" %s the following error occurred: %s"
|
|
391
|
+
),
|
|
373
392
|
url,
|
|
374
393
|
err,
|
|
375
394
|
)
|
|
@@ -381,9 +400,11 @@ class DenonAVRDeviceInfo:
|
|
|
381
400
|
self.model_name = "Unknown"
|
|
382
401
|
self.serial_number = None
|
|
383
402
|
_LOGGER.warning(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
403
|
+
(
|
|
404
|
+
"Unable to get device information of host %s, Device might be "
|
|
405
|
+
"in a corrupted state. Continuing without device information. "
|
|
406
|
+
"Disconnect and reconnect power to the device and try again."
|
|
407
|
+
),
|
|
387
408
|
self.api.host,
|
|
388
409
|
)
|
|
389
410
|
return
|
|
@@ -398,16 +419,17 @@ class DenonAVRDeviceInfo:
|
|
|
398
419
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
399
420
|
) -> None:
|
|
400
421
|
"""Update power status of device."""
|
|
401
|
-
if self.use_avr_2016_update is
|
|
422
|
+
if self.use_avr_2016_update is None:
|
|
423
|
+
raise AvrProcessingError(
|
|
424
|
+
"Device is not setup correctly, update method not set"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if self.use_avr_2016_update:
|
|
402
428
|
await self.async_update_power_appcommand(
|
|
403
429
|
global_update=global_update, cache_id=cache_id
|
|
404
430
|
)
|
|
405
|
-
elif self.use_avr_2016_update is False:
|
|
406
|
-
await self.async_update_power_status_xml(cache_id=cache_id)
|
|
407
431
|
else:
|
|
408
|
-
|
|
409
|
-
"Device is not setup correctly, update method not set"
|
|
410
|
-
)
|
|
432
|
+
await self.async_update_power_status_xml(cache_id=cache_id)
|
|
411
433
|
|
|
412
434
|
async def async_update_power_appcommand(
|
|
413
435
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
@@ -430,14 +452,12 @@ class DenonAVRDeviceInfo:
|
|
|
430
452
|
|
|
431
453
|
# Search for power tag
|
|
432
454
|
power_tag = xml.find(
|
|
433
|
-
"./cmd[@{
|
|
434
|
-
attribute=APPCOMMAND_CMD_TEXT, cmd=power_appcommand.cmd_text, zone=zone
|
|
435
|
-
)
|
|
455
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{power_appcommand.cmd_text}']/{zone}"
|
|
436
456
|
)
|
|
437
457
|
|
|
438
458
|
if power_tag is None:
|
|
439
459
|
raise AvrProcessingError(
|
|
440
|
-
"Power attribute of zone {} not found on update"
|
|
460
|
+
f"Power attribute of zone {self.zone} not found on update"
|
|
441
461
|
)
|
|
442
462
|
|
|
443
463
|
self._power = power_tag.text
|
|
@@ -451,7 +471,7 @@ class DenonAVRDeviceInfo:
|
|
|
451
471
|
if self.zone == MAIN_ZONE:
|
|
452
472
|
urls.append(self.urls.mainzone)
|
|
453
473
|
else:
|
|
454
|
-
urls.append("{}?ZoneName={
|
|
474
|
+
urls.append(f"{self.urls.mainzone}?ZoneName={self.zone}")
|
|
455
475
|
# Tags in XML which might contain information about zones power status
|
|
456
476
|
# ordered by their priority
|
|
457
477
|
tags = ["./ZonePower/value", "./Power/value"]
|
|
@@ -473,7 +493,7 @@ class DenonAVRDeviceInfo:
|
|
|
473
493
|
return
|
|
474
494
|
|
|
475
495
|
raise AvrProcessingError(
|
|
476
|
-
"Power attribute of zone {} not found on update"
|
|
496
|
+
f"Power attribute of zone {self.zone} not found on update"
|
|
477
497
|
)
|
|
478
498
|
|
|
479
499
|
##############
|
|
@@ -488,17 +508,32 @@ class DenonAVRDeviceInfo:
|
|
|
488
508
|
"""
|
|
489
509
|
return self._power
|
|
490
510
|
|
|
511
|
+
@property
|
|
512
|
+
def telnet_available(self) -> bool:
|
|
513
|
+
"""Return true if telnet is connected and healthy."""
|
|
514
|
+
return self.telnet_api.connected and self.telnet_api.healthy
|
|
515
|
+
|
|
491
516
|
##########
|
|
492
517
|
# Setter #
|
|
493
518
|
##########
|
|
494
519
|
|
|
495
520
|
async def async_power_on(self) -> None:
|
|
496
521
|
"""Turn on receiver via HTTP get command."""
|
|
497
|
-
|
|
522
|
+
if self.telnet_available:
|
|
523
|
+
await self.telnet_api.async_send_commands(
|
|
524
|
+
self.telnet_commands.command_power_on
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
await self.api.async_get_command(self.urls.command_power_on)
|
|
498
528
|
|
|
499
529
|
async def async_power_off(self) -> None:
|
|
500
530
|
"""Turn off receiver via HTTP get command."""
|
|
501
|
-
|
|
531
|
+
if self.telnet_available:
|
|
532
|
+
await self.telnet_api.async_send_commands(
|
|
533
|
+
self.telnet_commands.command_power_standby
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
await self.api.async_get_command(self.urls.command_power_standby)
|
|
502
537
|
|
|
503
538
|
|
|
504
539
|
@attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR)
|
|
@@ -592,13 +627,12 @@ class DenonAVRFoundation:
|
|
|
592
627
|
update_attrs.pop(app_command, None)
|
|
593
628
|
|
|
594
629
|
# Check if each attribute was updated
|
|
595
|
-
if update_attrs and ignore_missing_response
|
|
630
|
+
if update_attrs and not ignore_missing_response:
|
|
596
631
|
raise AvrProcessingError(
|
|
597
|
-
"Some attributes of zone {} not found on update:
|
|
598
|
-
|
|
599
|
-
)
|
|
632
|
+
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
633
|
+
f" {update_attrs}"
|
|
600
634
|
)
|
|
601
|
-
if update_attrs and ignore_missing_response
|
|
635
|
+
if update_attrs and ignore_missing_response:
|
|
602
636
|
_LOGGER.debug(
|
|
603
637
|
"Some attributes of zone %s not found on update: %s",
|
|
604
638
|
self._device.zone,
|
|
@@ -660,11 +694,10 @@ class DenonAVRFoundation:
|
|
|
660
694
|
break
|
|
661
695
|
|
|
662
696
|
# Check if each attribute was updated
|
|
663
|
-
if update_attrs and ignore_missing_response
|
|
697
|
+
if update_attrs and not ignore_missing_response:
|
|
664
698
|
raise AvrProcessingError(
|
|
665
|
-
"Some attributes of zone {} not found on update:
|
|
666
|
-
|
|
667
|
-
)
|
|
699
|
+
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
700
|
+
f" {update_attrs}"
|
|
668
701
|
)
|
|
669
702
|
|
|
670
703
|
@staticmethod
|
|
@@ -678,17 +711,15 @@ class DenonAVRFoundation:
|
|
|
678
711
|
string = "./cmd"
|
|
679
712
|
# Text of cmd tag in query was added as attribute to response
|
|
680
713
|
if app_command_cmd.cmd_text:
|
|
681
|
-
string =
|
|
682
|
-
APPCOMMAND_CMD_TEXT
|
|
714
|
+
string = (
|
|
715
|
+
string + f"[@{APPCOMMAND_CMD_TEXT}='{app_command_cmd.cmd_text}']"
|
|
683
716
|
)
|
|
684
717
|
# Text of name tag in query was added as attribute to response
|
|
685
718
|
if app_command_cmd.name:
|
|
686
|
-
string = string + "[@{}='{}']"
|
|
687
|
-
APPCOMMAND_NAME, app_command_cmd.name
|
|
688
|
-
)
|
|
719
|
+
string = string + f"[@{APPCOMMAND_NAME}='{app_command_cmd.name}']"
|
|
689
720
|
# Some results include a zone tag
|
|
690
721
|
if resp.add_zone:
|
|
691
|
-
string = string + "/{}"
|
|
722
|
+
string = string + f"/{zone}"
|
|
692
723
|
# Suffix like /status, /volume
|
|
693
724
|
string = string + resp.suffix
|
|
694
725
|
|
|
@@ -722,8 +753,10 @@ def set_api_timeout(
|
|
|
722
753
|
return value
|
|
723
754
|
|
|
724
755
|
|
|
725
|
-
def convert_string_int_bool(value: str) -> bool:
|
|
756
|
+
def convert_string_int_bool(value: Union[str, bool]) -> bool:
|
|
726
757
|
"""Convert an integer from string format to bool."""
|
|
727
758
|
if value is None:
|
|
728
759
|
return None
|
|
760
|
+
if isinstance(value, bool):
|
|
761
|
+
return value
|
|
729
762
|
return bool(int(value))
|