denonavr 0.11.3__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 +144 -49
- denonavr/audyssey.py +59 -21
- denonavr/const.py +93 -10
- denonavr/decorators.py +27 -41
- denonavr/denonavr.py +50 -7
- denonavr/foundation.py +51 -41
- denonavr/input.py +49 -45
- denonavr/soundmode.py +14 -8
- denonavr/ssdp.py +13 -13
- denonavr/tonecontrol.py +83 -27
- denonavr/volume.py +29 -23
- {denonavr-0.11.3.dist-info → denonavr-0.11.6.dist-info}/METADATA +12 -10
- denonavr-0.11.6.dist-info/RECORD +19 -0
- {denonavr-0.11.3.dist-info → denonavr-0.11.6.dist-info}/WHEEL +1 -1
- denonavr-0.11.3.dist-info/RECORD +0 -19
- {denonavr-0.11.3.dist-info → denonavr-0.11.6.dist-info}/LICENSE +0 -0
- {denonavr-0.11.3.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
|
|
@@ -119,7 +120,7 @@ class DenonAVRDeviceInfo:
|
|
|
119
120
|
self.telnet_commands = ZONE3_TELNET_COMMANDS
|
|
120
121
|
self.urls = ZONE3_URLS
|
|
121
122
|
else:
|
|
122
|
-
raise ValueError("Invalid zone {
|
|
123
|
+
raise ValueError(f"Invalid zone {self.zone}")
|
|
123
124
|
|
|
124
125
|
async def _async_power_callback(
|
|
125
126
|
self, zone: str, event: str, parameter: str
|
|
@@ -179,7 +180,7 @@ class DenonAVRDeviceInfo:
|
|
|
179
180
|
|
|
180
181
|
async def async_identify_receiver(self) -> None:
|
|
181
182
|
"""Identify receiver asynchronously."""
|
|
182
|
-
# Test Deviceinfo.xml if receiver is
|
|
183
|
+
# Test Deviceinfo.xml if receiver is an AVR-X with port 80 for pre 2016
|
|
183
184
|
# devices and port 8080 devices 2016 and later
|
|
184
185
|
# 2016 models has also some of the XML but not all, try first 2016
|
|
185
186
|
r_types = [AVR_X, AVR_X_2016]
|
|
@@ -200,15 +201,17 @@ class DenonAVRDeviceInfo:
|
|
|
200
201
|
exc_info=err,
|
|
201
202
|
)
|
|
202
203
|
|
|
203
|
-
# Raise error only when
|
|
204
|
+
# Raise error only when occurred at both types
|
|
204
205
|
timeout_errors += 1
|
|
205
206
|
if timeout_errors == len(r_types):
|
|
206
207
|
raise
|
|
207
208
|
|
|
208
209
|
except AvrRequestError as err:
|
|
209
210
|
_LOGGER.debug(
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
(
|
|
212
|
+
"Request error on port %s when identifying receiver, "
|
|
213
|
+
"device is not a %s receivers"
|
|
214
|
+
),
|
|
212
215
|
r_type.port,
|
|
213
216
|
r_type.type,
|
|
214
217
|
exc_info=err,
|
|
@@ -220,13 +223,13 @@ class DenonAVRDeviceInfo:
|
|
|
220
223
|
# Receiver identified, return
|
|
221
224
|
return
|
|
222
225
|
|
|
223
|
-
# If check of Deviceinfo.xml was not
|
|
226
|
+
# If check of Deviceinfo.xml was not successful, receiver is type AVR
|
|
224
227
|
self.receiver = AVR
|
|
225
228
|
self.api.port = AVR.port
|
|
226
229
|
|
|
227
230
|
@staticmethod
|
|
228
231
|
def _is_avr_x(deviceinfo: ET.Element) -> bool:
|
|
229
|
-
"""Evaluate Deviceinfo.xml if the device is
|
|
232
|
+
"""Evaluate Deviceinfo.xml if the device is an AVR-X device."""
|
|
230
233
|
# First test by CommApiVers
|
|
231
234
|
try:
|
|
232
235
|
if bool(
|
|
@@ -369,9 +372,7 @@ class DenonAVRDeviceInfo:
|
|
|
369
372
|
"""Get device information."""
|
|
370
373
|
port = DESCRIPTION_TYPES[self.receiver.type].port
|
|
371
374
|
command = DESCRIPTION_TYPES[self.receiver.type].url
|
|
372
|
-
url = "http://{host}:{port}{command}"
|
|
373
|
-
host=self.api.host, port=port, command=command
|
|
374
|
-
)
|
|
375
|
+
url = f"http://{self.api.host}:{port}{command}"
|
|
375
376
|
|
|
376
377
|
device_info = None
|
|
377
378
|
try:
|
|
@@ -384,8 +385,10 @@ class DenonAVRDeviceInfo:
|
|
|
384
385
|
raise
|
|
385
386
|
except AvrRequestError as err:
|
|
386
387
|
_LOGGER.error(
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
(
|
|
389
|
+
"During DenonAVR device identification, when trying to request"
|
|
390
|
+
" %s the following error occurred: %s"
|
|
391
|
+
),
|
|
389
392
|
url,
|
|
390
393
|
err,
|
|
391
394
|
)
|
|
@@ -397,9 +400,11 @@ class DenonAVRDeviceInfo:
|
|
|
397
400
|
self.model_name = "Unknown"
|
|
398
401
|
self.serial_number = None
|
|
399
402
|
_LOGGER.warning(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
),
|
|
403
408
|
self.api.host,
|
|
404
409
|
)
|
|
405
410
|
return
|
|
@@ -447,14 +452,12 @@ class DenonAVRDeviceInfo:
|
|
|
447
452
|
|
|
448
453
|
# Search for power tag
|
|
449
454
|
power_tag = xml.find(
|
|
450
|
-
"./cmd[@{
|
|
451
|
-
attribute=APPCOMMAND_CMD_TEXT, cmd=power_appcommand.cmd_text, zone=zone
|
|
452
|
-
)
|
|
455
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{power_appcommand.cmd_text}']/{zone}"
|
|
453
456
|
)
|
|
454
457
|
|
|
455
458
|
if power_tag is None:
|
|
456
459
|
raise AvrProcessingError(
|
|
457
|
-
"Power attribute of zone {} not found on update"
|
|
460
|
+
f"Power attribute of zone {self.zone} not found on update"
|
|
458
461
|
)
|
|
459
462
|
|
|
460
463
|
self._power = power_tag.text
|
|
@@ -468,7 +471,7 @@ class DenonAVRDeviceInfo:
|
|
|
468
471
|
if self.zone == MAIN_ZONE:
|
|
469
472
|
urls.append(self.urls.mainzone)
|
|
470
473
|
else:
|
|
471
|
-
urls.append("{}?ZoneName={
|
|
474
|
+
urls.append(f"{self.urls.mainzone}?ZoneName={self.zone}")
|
|
472
475
|
# Tags in XML which might contain information about zones power status
|
|
473
476
|
# ordered by their priority
|
|
474
477
|
tags = ["./ZonePower/value", "./Power/value"]
|
|
@@ -490,7 +493,7 @@ class DenonAVRDeviceInfo:
|
|
|
490
493
|
return
|
|
491
494
|
|
|
492
495
|
raise AvrProcessingError(
|
|
493
|
-
"Power attribute of zone {} not found on update"
|
|
496
|
+
f"Power attribute of zone {self.zone} not found on update"
|
|
494
497
|
)
|
|
495
498
|
|
|
496
499
|
##############
|
|
@@ -505,22 +508,31 @@ class DenonAVRDeviceInfo:
|
|
|
505
508
|
"""
|
|
506
509
|
return self._power
|
|
507
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
|
+
|
|
508
516
|
##########
|
|
509
517
|
# Setter #
|
|
510
518
|
##########
|
|
511
519
|
|
|
512
520
|
async def async_power_on(self) -> None:
|
|
513
521
|
"""Turn on receiver via HTTP get command."""
|
|
514
|
-
|
|
515
|
-
|
|
522
|
+
if self.telnet_available:
|
|
523
|
+
await self.telnet_api.async_send_commands(
|
|
524
|
+
self.telnet_commands.command_power_on
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
516
527
|
await self.api.async_get_command(self.urls.command_power_on)
|
|
517
528
|
|
|
518
529
|
async def async_power_off(self) -> None:
|
|
519
530
|
"""Turn off receiver via HTTP get command."""
|
|
520
|
-
|
|
521
|
-
self.
|
|
522
|
-
|
|
523
|
-
|
|
531
|
+
if self.telnet_available:
|
|
532
|
+
await self.telnet_api.async_send_commands(
|
|
533
|
+
self.telnet_commands.command_power_standby
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
524
536
|
await self.api.async_get_command(self.urls.command_power_standby)
|
|
525
537
|
|
|
526
538
|
|
|
@@ -617,9 +629,8 @@ class DenonAVRFoundation:
|
|
|
617
629
|
# Check if each attribute was updated
|
|
618
630
|
if update_attrs and not ignore_missing_response:
|
|
619
631
|
raise AvrProcessingError(
|
|
620
|
-
"Some attributes of zone {} not found on update:
|
|
621
|
-
|
|
622
|
-
)
|
|
632
|
+
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
633
|
+
f" {update_attrs}"
|
|
623
634
|
)
|
|
624
635
|
if update_attrs and ignore_missing_response:
|
|
625
636
|
_LOGGER.debug(
|
|
@@ -685,9 +696,8 @@ class DenonAVRFoundation:
|
|
|
685
696
|
# Check if each attribute was updated
|
|
686
697
|
if update_attrs and not ignore_missing_response:
|
|
687
698
|
raise AvrProcessingError(
|
|
688
|
-
"Some attributes of zone {} not found on update:
|
|
689
|
-
|
|
690
|
-
)
|
|
699
|
+
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
700
|
+
f" {update_attrs}"
|
|
691
701
|
)
|
|
692
702
|
|
|
693
703
|
@staticmethod
|
|
@@ -701,17 +711,15 @@ class DenonAVRFoundation:
|
|
|
701
711
|
string = "./cmd"
|
|
702
712
|
# Text of cmd tag in query was added as attribute to response
|
|
703
713
|
if app_command_cmd.cmd_text:
|
|
704
|
-
string =
|
|
705
|
-
APPCOMMAND_CMD_TEXT
|
|
714
|
+
string = (
|
|
715
|
+
string + f"[@{APPCOMMAND_CMD_TEXT}='{app_command_cmd.cmd_text}']"
|
|
706
716
|
)
|
|
707
717
|
# Text of name tag in query was added as attribute to response
|
|
708
718
|
if app_command_cmd.name:
|
|
709
|
-
string = string + "[@{}='{}']"
|
|
710
|
-
APPCOMMAND_NAME, app_command_cmd.name
|
|
711
|
-
)
|
|
719
|
+
string = string + f"[@{APPCOMMAND_NAME}='{app_command_cmd.name}']"
|
|
712
720
|
# Some results include a zone tag
|
|
713
721
|
if resp.add_zone:
|
|
714
|
-
string = string + "/{}"
|
|
722
|
+
string = string + f"/{zone}"
|
|
715
723
|
# Suffix like /status, /volume
|
|
716
724
|
string = string + resp.suffix
|
|
717
725
|
|
|
@@ -745,8 +753,10 @@ def set_api_timeout(
|
|
|
745
753
|
return value
|
|
746
754
|
|
|
747
755
|
|
|
748
|
-
def convert_string_int_bool(value: str) -> bool:
|
|
756
|
+
def convert_string_int_bool(value: Union[str, bool]) -> bool:
|
|
749
757
|
"""Convert an integer from string format to bool."""
|
|
750
758
|
if value is None:
|
|
751
759
|
return None
|
|
760
|
+
if isinstance(value, bool):
|
|
761
|
+
return value
|
|
752
762
|
return bool(int(value))
|
denonavr/input.py
CHANGED
|
@@ -8,13 +8,14 @@ This module implements the handler for input functions of Denon AVR receivers.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
-
import html
|
|
12
11
|
import logging
|
|
12
|
+
from collections.abc import Hashable
|
|
13
13
|
from copy import deepcopy
|
|
14
|
-
from typing import Dict,
|
|
14
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
15
15
|
|
|
16
16
|
import attr
|
|
17
17
|
import httpx
|
|
18
|
+
from ftfy import fix_text
|
|
18
19
|
|
|
19
20
|
from .appcommand import AppCommands
|
|
20
21
|
from .const import (
|
|
@@ -56,11 +57,11 @@ def lower_string(value: Optional[str]) -> Optional[str]:
|
|
|
56
57
|
return str(value).lower()
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
-
"""
|
|
60
|
+
def fix_string(value: Optional[str]) -> Optional[str]:
|
|
61
|
+
"""Fix errors in string like unescaped HTML and wrong utf-8 encoding."""
|
|
61
62
|
if value is None:
|
|
62
63
|
return value
|
|
63
|
-
return
|
|
64
|
+
return fix_text(str(value)).strip()
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def set_input_func(
|
|
@@ -143,22 +144,22 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
143
144
|
)
|
|
144
145
|
|
|
145
146
|
_artist: Optional[str] = attr.ib(
|
|
146
|
-
converter=attr.converters.optional(
|
|
147
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
147
148
|
)
|
|
148
149
|
_album: Optional[str] = attr.ib(
|
|
149
|
-
converter=attr.converters.optional(
|
|
150
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
150
151
|
)
|
|
151
152
|
_band: Optional[str] = attr.ib(
|
|
152
|
-
converter=attr.converters.optional(
|
|
153
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
153
154
|
)
|
|
154
155
|
_title: Optional[str] = attr.ib(
|
|
155
|
-
converter=attr.converters.optional(
|
|
156
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
156
157
|
)
|
|
157
158
|
_frequency: Optional[str] = attr.ib(
|
|
158
|
-
converter=attr.converters.optional(
|
|
159
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
159
160
|
)
|
|
160
161
|
_station: Optional[str] = attr.ib(
|
|
161
|
-
converter=attr.converters.optional(
|
|
162
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
162
163
|
)
|
|
163
164
|
_image_url: Optional[str] = attr.ib(
|
|
164
165
|
converter=attr.converters.optional(str), default=None
|
|
@@ -271,7 +272,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
271
272
|
|
|
272
273
|
def _update_netaudio(self) -> None:
|
|
273
274
|
"""Update netaudio information."""
|
|
274
|
-
if self._device.
|
|
275
|
+
if self._device.telnet_available:
|
|
276
|
+
self._device.telnet_api.send_commands("NSE")
|
|
275
277
|
self._schedule_netaudio_update()
|
|
276
278
|
else:
|
|
277
279
|
self._stop_media_update()
|
|
@@ -301,7 +303,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
301
303
|
port=self._device.api.port,
|
|
302
304
|
hash=hash((self._title, self._artist, self._album)),
|
|
303
305
|
)
|
|
304
|
-
await self.
|
|
306
|
+
await self._async_test_image_accessible()
|
|
305
307
|
|
|
306
308
|
def _schedule_tuner_update(self) -> None:
|
|
307
309
|
"""Schedule a tuner update task."""
|
|
@@ -313,7 +315,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
313
315
|
|
|
314
316
|
def _update_tuner(self) -> None:
|
|
315
317
|
"""Update tuner information."""
|
|
316
|
-
if self._device.
|
|
318
|
+
if self._device.telnet_available:
|
|
319
|
+
self._device.telnet_api.send_commands("TFAN?", "TFANNAME?")
|
|
317
320
|
self._schedule_tuner_update()
|
|
318
321
|
else:
|
|
319
322
|
self._stop_media_update()
|
|
@@ -330,7 +333,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
330
333
|
if parameter.startswith("ANNAME"):
|
|
331
334
|
self._station = parameter[6:]
|
|
332
335
|
elif len(parameter) == 8:
|
|
333
|
-
self._frequency = "{
|
|
336
|
+
self._frequency = f"{parameter[2:6]}.{parameter[6:]}".strip("0")
|
|
334
337
|
if parameter[2:] > "050000":
|
|
335
338
|
self._band = "AM"
|
|
336
339
|
else:
|
|
@@ -344,7 +347,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
344
347
|
self._image_url = STATIC_ALBUM_URL.format(
|
|
345
348
|
host=self._device.api.host, port=self._device.api.port
|
|
346
349
|
)
|
|
347
|
-
await self.
|
|
350
|
+
await self._async_test_image_accessible()
|
|
348
351
|
|
|
349
352
|
def _schedule_hdtuner_update(self) -> None:
|
|
350
353
|
"""Schedule a HD tuner update task."""
|
|
@@ -356,7 +359,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
356
359
|
|
|
357
360
|
def _update_hdtuner(self) -> None:
|
|
358
361
|
"""Update HD tuner information."""
|
|
359
|
-
if self._device.
|
|
362
|
+
if self._device.telnet_available:
|
|
363
|
+
self._device.telnet_api.send_commands("HD?")
|
|
360
364
|
self._schedule_hdtuner_update()
|
|
361
365
|
else:
|
|
362
366
|
self._stop_media_update()
|
|
@@ -386,7 +390,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
386
390
|
self._image_url = STATIC_ALBUM_URL.format(
|
|
387
391
|
host=self._device.api.host, port=self._device.api.port
|
|
388
392
|
)
|
|
389
|
-
await self.
|
|
393
|
+
await self._async_test_image_accessible()
|
|
390
394
|
|
|
391
395
|
async def _async_input_func_update_callback(
|
|
392
396
|
self, zone: str, event: str, parameter: str
|
|
@@ -447,9 +451,9 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
447
451
|
# Get list of all input sources of receiver
|
|
448
452
|
xml_list = xml_zonecapa.find("./InputSource/List")
|
|
449
453
|
for xml_source in xml_list.findall("Source"):
|
|
450
|
-
receiver_sources[
|
|
451
|
-
xml_source.find("
|
|
452
|
-
|
|
454
|
+
receiver_sources[xml_source.find("FuncName").text] = (
|
|
455
|
+
xml_source.find("DefaultName").text
|
|
456
|
+
)
|
|
453
457
|
|
|
454
458
|
return receiver_sources
|
|
455
459
|
|
|
@@ -484,9 +488,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
484
488
|
raise
|
|
485
489
|
|
|
486
490
|
for child in xml.findall(
|
|
487
|
-
"./cmd[@{
|
|
488
|
-
|
|
489
|
-
)
|
|
491
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{AppCommands.GetRenameSource.cmd_text}']"
|
|
492
|
+
"/functionrename/list"
|
|
490
493
|
):
|
|
491
494
|
try:
|
|
492
495
|
renamed_sources[child.find("name").text.strip()] = child.find(
|
|
@@ -496,9 +499,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
496
499
|
continue
|
|
497
500
|
|
|
498
501
|
for child in xml.findall(
|
|
499
|
-
"./cmd[@{
|
|
500
|
-
|
|
501
|
-
)
|
|
502
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{AppCommands.GetDeletedSource.cmd_text}']"
|
|
503
|
+
"/functiondelete/list"
|
|
502
504
|
):
|
|
503
505
|
try:
|
|
504
506
|
deleted_sources[child.find("FuncName").text.strip()] = (
|
|
@@ -541,9 +543,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
541
543
|
)
|
|
542
544
|
else:
|
|
543
545
|
raise AvrProcessingError(
|
|
544
|
-
"Method does not work for receiver type
|
|
545
|
-
|
|
546
|
-
)
|
|
546
|
+
"Method does not work for receiver type"
|
|
547
|
+
f" {self._device.receiver.type}"
|
|
547
548
|
)
|
|
548
549
|
except AvrRequestError as err:
|
|
549
550
|
_LOGGER.debug("Error when getting changed sources", exc_info=err)
|
|
@@ -668,7 +669,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
668
669
|
playing_func_list = []
|
|
669
670
|
|
|
670
671
|
for item in receiver_sources.items():
|
|
671
|
-
# Mapping of item[0] because some func names are
|
|
672
|
+
# Mapping of item[0] because some func names are inconsistent
|
|
672
673
|
# at AVR-X receivers
|
|
673
674
|
|
|
674
675
|
m_item_0 = SOURCE_MAPPING.get(item[0], item[0])
|
|
@@ -846,10 +847,10 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
846
847
|
# On track change assume device is PLAYING
|
|
847
848
|
self._state = STATE_PLAYING
|
|
848
849
|
|
|
849
|
-
await self.
|
|
850
|
+
await self._async_test_image_accessible()
|
|
850
851
|
|
|
851
|
-
async def
|
|
852
|
-
"""Test if image URL is
|
|
852
|
+
async def _async_test_image_accessible(self) -> None:
|
|
853
|
+
"""Test if image URL is accessible."""
|
|
853
854
|
if self._image_available is None and self._image_url is not None:
|
|
854
855
|
client = self._device.api.async_client_getter()
|
|
855
856
|
try:
|
|
@@ -995,7 +996,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
995
996
|
linp = self._input_func_map[input_func]
|
|
996
997
|
except KeyError as err:
|
|
997
998
|
raise AvrCommandError(
|
|
998
|
-
"No mapping for input source {}"
|
|
999
|
+
f"No mapping for input source {input_func}"
|
|
999
1000
|
) from err
|
|
1000
1001
|
# Create command URL and send command via HTTP GET
|
|
1001
1002
|
if linp in self._favorite_func_list:
|
|
@@ -1004,8 +1005,9 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
1004
1005
|
else:
|
|
1005
1006
|
command_url = self._device.urls.command_sel_src + linp
|
|
1006
1007
|
telnet_command = self._device.telnet_commands.command_sel_src + linp
|
|
1007
|
-
|
|
1008
|
-
|
|
1008
|
+
if self._device.telnet_available:
|
|
1009
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
1010
|
+
else:
|
|
1009
1011
|
await self._device.api.async_get_command(command_url)
|
|
1010
1012
|
|
|
1011
1013
|
async def async_toggle_play_pause(self) -> None:
|
|
@@ -1027,10 +1029,11 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
1027
1029
|
if self._state == STATE_PLAYING:
|
|
1028
1030
|
_LOGGER.info("Already playing, play command not sent")
|
|
1029
1031
|
return
|
|
1030
|
-
|
|
1031
|
-
self._device.
|
|
1032
|
-
|
|
1033
|
-
|
|
1032
|
+
if self._device.telnet_available:
|
|
1033
|
+
await self._device.telnet_api.async_send_commands(
|
|
1034
|
+
self._device.telnet_commands.command_play
|
|
1035
|
+
)
|
|
1036
|
+
else:
|
|
1034
1037
|
await self._device.api.async_get_command(self._device.urls.command_play)
|
|
1035
1038
|
self._state = STATE_PLAYING
|
|
1036
1039
|
|
|
@@ -1038,10 +1041,11 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
1038
1041
|
"""Send pause command to receiver command via HTTP post."""
|
|
1039
1042
|
# Use pause command only for sources which support NETAUDIO
|
|
1040
1043
|
if self._input_func in self._netaudio_func_list:
|
|
1041
|
-
|
|
1042
|
-
self._device.
|
|
1043
|
-
|
|
1044
|
-
|
|
1044
|
+
if self._device.telnet_available:
|
|
1045
|
+
await self._device.telnet_api.async_send_commands(
|
|
1046
|
+
self._device.telnet_commands.command_play
|
|
1047
|
+
)
|
|
1048
|
+
else:
|
|
1045
1049
|
await self._device.api.async_get_command(
|
|
1046
1050
|
self._device.urls.command_pause
|
|
1047
1051
|
)
|
denonavr/soundmode.py
CHANGED
|
@@ -9,8 +9,9 @@ This module implements the handler for sound mode of Denon AVR receivers.
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
|
+
from collections.abc import Hashable
|
|
12
13
|
from copy import deepcopy
|
|
13
|
-
from typing import Dict,
|
|
14
|
+
from typing import Dict, List, Optional
|
|
14
15
|
|
|
15
16
|
import attr
|
|
16
17
|
|
|
@@ -22,7 +23,7 @@ from .const import (
|
|
|
22
23
|
DENON_ATTR_SETATTR,
|
|
23
24
|
SOUND_MODE_MAPPING,
|
|
24
25
|
)
|
|
25
|
-
from .exceptions import AvrProcessingError
|
|
26
|
+
from .exceptions import AvrCommandError, AvrProcessingError
|
|
26
27
|
from .foundation import DenonAVRFoundation
|
|
27
28
|
|
|
28
29
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -109,7 +110,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
109
110
|
self._device.api.add_appcommand_update_tag(tag)
|
|
110
111
|
|
|
111
112
|
# Soundmode is always available for AVR-X and AVR-X-2016 receivers
|
|
112
|
-
# For AVR receiver it will be tested
|
|
113
|
+
# For AVR receiver it will be tested during the first update
|
|
113
114
|
if self._device.receiver in [AVR_X, AVR_X_2016]:
|
|
114
115
|
self._support_sound_mode = True
|
|
115
116
|
else:
|
|
@@ -237,8 +238,9 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
237
238
|
else:
|
|
238
239
|
command_url += "ZST OFF"
|
|
239
240
|
telnet_command += "ZST OFF"
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
if self._device.telnet_available:
|
|
242
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
243
|
+
else:
|
|
242
244
|
await self._device.api.async_get_command(command_url)
|
|
243
245
|
|
|
244
246
|
##############
|
|
@@ -256,7 +258,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
256
258
|
return sound_mode_matched
|
|
257
259
|
|
|
258
260
|
@property
|
|
259
|
-
def sound_mode_list(self) ->
|
|
261
|
+
def sound_mode_list(self) -> List[str]:
|
|
260
262
|
"""Return a list of available sound modes as string."""
|
|
261
263
|
return list(self._sound_mode_map.keys())
|
|
262
264
|
|
|
@@ -285,6 +287,9 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
285
287
|
Valid values depend on the device and should be taken from
|
|
286
288
|
"sound_mode_list".
|
|
287
289
|
"""
|
|
290
|
+
if sound_mode not in self.sound_mode_list:
|
|
291
|
+
raise AvrCommandError(f"{sound_mode} is not a valid sound mode")
|
|
292
|
+
|
|
288
293
|
if sound_mode == ALL_ZONE_STEREO:
|
|
289
294
|
await self._async_set_all_zone_stereo(True)
|
|
290
295
|
return
|
|
@@ -300,8 +305,9 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
300
305
|
self._device.telnet_commands.command_sel_sound_mode + sound_mode
|
|
301
306
|
)
|
|
302
307
|
# sent command
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
if self._device.telnet_available:
|
|
309
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
310
|
+
else:
|
|
305
311
|
await self._device.api.async_get_command(command_url)
|
|
306
312
|
|
|
307
313
|
|
denonavr/ssdp.py
CHANGED
|
@@ -35,14 +35,14 @@ SSDP_ST_LIST = (SSDP_ST_1, SSDP_ST_2, SSDP_ST_3)
|
|
|
35
35
|
SSDP_LOCATION_PATTERN = re.compile(r"(?<=LOCATION:\s).+?(?=\r)")
|
|
36
36
|
|
|
37
37
|
SCPD_XMLNS = "{urn:schemas-upnp-org:device-1-0}"
|
|
38
|
-
SCPD_DEVICE = "{
|
|
39
|
-
SCPD_DEVICELIST = "{
|
|
40
|
-
SCPD_DEVICETYPE = "{
|
|
41
|
-
SCPD_MANUFACTURER = "{
|
|
42
|
-
SCPD_MODELNAME = "{
|
|
43
|
-
SCPD_SERIALNUMBER = "{
|
|
44
|
-
SCPD_FRIENDLYNAME = "{
|
|
45
|
-
SCPD_PRESENTATIONURL = "{
|
|
38
|
+
SCPD_DEVICE = f"{SCPD_XMLNS}device"
|
|
39
|
+
SCPD_DEVICELIST = f"{SCPD_XMLNS}deviceList"
|
|
40
|
+
SCPD_DEVICETYPE = f"{SCPD_XMLNS}deviceType"
|
|
41
|
+
SCPD_MANUFACTURER = f"{SCPD_XMLNS}manufacturer"
|
|
42
|
+
SCPD_MODELNAME = f"{SCPD_XMLNS}modelName"
|
|
43
|
+
SCPD_SERIALNUMBER = f"{SCPD_XMLNS}serialNumber"
|
|
44
|
+
SCPD_FRIENDLYNAME = f"{SCPD_XMLNS}friendlyName"
|
|
45
|
+
SCPD_PRESENTATIONURL = f"{SCPD_XMLNS}presentationURL"
|
|
46
46
|
|
|
47
47
|
SUPPORTED_DEVICETYPES = [
|
|
48
48
|
"urn:schemas-upnp-org:device:MediaRenderer:1",
|
|
@@ -57,10 +57,10 @@ def ssdp_request(ssdp_st: str, ssdp_mx: float = SSDP_MX) -> bytes:
|
|
|
57
57
|
return "\r\n".join(
|
|
58
58
|
[
|
|
59
59
|
"M-SEARCH * HTTP/1.1",
|
|
60
|
-
"ST: {}"
|
|
61
|
-
"MX: {:d}"
|
|
60
|
+
f"ST: {ssdp_st}",
|
|
61
|
+
f"MX: {ssdp_mx:d}",
|
|
62
62
|
'MAN: "ssdp:discover"',
|
|
63
|
-
"HOST: {}:{}"
|
|
63
|
+
f"HOST: {SSDP_ADDR}:{SSDP_PORT}",
|
|
64
64
|
"",
|
|
65
65
|
"",
|
|
66
66
|
]
|
|
@@ -133,8 +133,8 @@ async def async_send_ssdp_broadcast() -> Set[str]:
|
|
|
133
133
|
|
|
134
134
|
async def async_send_ssdp_broadcast_ip(ip_addr: str) -> Set[str]:
|
|
135
135
|
"""Send SSDP broadcast messages to a single IP."""
|
|
136
|
-
# Ignore 169.254.0.0/16
|
|
137
|
-
if
|
|
136
|
+
# Ignore 169.254.0.0/16 addresses
|
|
137
|
+
if ip_addr.startswith("169.254."):
|
|
138
138
|
return set()
|
|
139
139
|
|
|
140
140
|
# Prepare socket
|