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/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,15 +57,15 @@ 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(
|
|
67
|
-
instance:
|
|
68
|
+
instance: "DenonAVRInput", attribute: attr.Attribute, value: str
|
|
68
69
|
) -> str:
|
|
69
70
|
"""Set input_func after mapping from input_func_map."""
|
|
70
71
|
# pylint: disable=protected-access
|
|
@@ -75,8 +76,11 @@ def set_input_func(
|
|
|
75
76
|
# AirPlay and Internet Radio are not always listed in available sources
|
|
76
77
|
if value in ["AirPlay", "Internet Radio"]:
|
|
77
78
|
if value not in instance._input_func_map:
|
|
79
|
+
instance._additional_input_funcs.add(value)
|
|
78
80
|
instance._input_func_map[value] = value
|
|
79
81
|
instance._input_func_map_rev[value] = value
|
|
82
|
+
instance._netaudio_func_list.append(value)
|
|
83
|
+
instance._playing_func_list.append(value)
|
|
80
84
|
try:
|
|
81
85
|
input_func = instance._input_func_map_rev[value]
|
|
82
86
|
except KeyError:
|
|
@@ -121,6 +125,12 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
121
125
|
),
|
|
122
126
|
default=attr.Factory(list),
|
|
123
127
|
)
|
|
128
|
+
_additional_input_funcs: Set[str] = attr.ib(
|
|
129
|
+
validator=attr.validators.deep_iterable(
|
|
130
|
+
attr.validators.instance_of(str), attr.validators.instance_of(set)
|
|
131
|
+
),
|
|
132
|
+
default=attr.Factory(set),
|
|
133
|
+
)
|
|
124
134
|
_media_update_handle: asyncio.TimerHandle = attr.ib(default=None)
|
|
125
135
|
_input_func_update_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
|
|
126
136
|
|
|
@@ -134,22 +144,22 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
134
144
|
)
|
|
135
145
|
|
|
136
146
|
_artist: Optional[str] = attr.ib(
|
|
137
|
-
converter=attr.converters.optional(
|
|
147
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
138
148
|
)
|
|
139
149
|
_album: Optional[str] = attr.ib(
|
|
140
|
-
converter=attr.converters.optional(
|
|
150
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
141
151
|
)
|
|
142
152
|
_band: Optional[str] = attr.ib(
|
|
143
|
-
converter=attr.converters.optional(
|
|
153
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
144
154
|
)
|
|
145
155
|
_title: Optional[str] = attr.ib(
|
|
146
|
-
converter=attr.converters.optional(
|
|
156
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
147
157
|
)
|
|
148
158
|
_frequency: Optional[str] = attr.ib(
|
|
149
|
-
converter=attr.converters.optional(
|
|
159
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
150
160
|
)
|
|
151
161
|
_station: Optional[str] = attr.ib(
|
|
152
|
-
converter=attr.converters.optional(
|
|
162
|
+
converter=attr.converters.optional(fix_string), default=None
|
|
153
163
|
)
|
|
154
164
|
_image_url: Optional[str] = attr.ib(
|
|
155
165
|
converter=attr.converters.optional(str), default=None
|
|
@@ -175,9 +185,16 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
175
185
|
for tag in self.appcommand_attrs:
|
|
176
186
|
self._device.api.add_appcommand_update_tag(tag)
|
|
177
187
|
|
|
188
|
+
power_event = "ZM"
|
|
189
|
+
if self._device.zone == ZONE2:
|
|
190
|
+
power_event = "Z2"
|
|
191
|
+
elif self._device.zone == ZONE3:
|
|
192
|
+
power_event = "Z3"
|
|
193
|
+
self._device.telnet_api.register_callback(
|
|
194
|
+
power_event, self._async_power_callback
|
|
195
|
+
)
|
|
178
196
|
self._device.telnet_api.register_callback("SI", self._async_input_callback)
|
|
179
|
-
self._device.telnet_api.register_callback("
|
|
180
|
-
self._device.telnet_api.register_callback("NS", self._async_netaudio_callback)
|
|
197
|
+
self._device.telnet_api.register_callback("NSE", self._async_netaudio_callback)
|
|
181
198
|
self._device.telnet_api.register_callback("TF", self._async_tuner_callback)
|
|
182
199
|
self._device.telnet_api.register_callback("HD", self._async_hdtuner_callback)
|
|
183
200
|
self._device.telnet_api.register_callback(
|
|
@@ -198,7 +215,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
198
215
|
if self._device.power != POWER_ON:
|
|
199
216
|
return
|
|
200
217
|
|
|
201
|
-
if self._schedule_media_updates()
|
|
218
|
+
if self._schedule_media_updates():
|
|
202
219
|
self._state = STATE_PLAYING
|
|
203
220
|
else:
|
|
204
221
|
self._unset_media_state()
|
|
@@ -215,7 +232,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
215
232
|
self._stop_media_update()
|
|
216
233
|
self._unset_media_state()
|
|
217
234
|
self._state = STATE_OFF
|
|
218
|
-
elif self._schedule_media_updates()
|
|
235
|
+
elif self._schedule_media_updates():
|
|
219
236
|
self._state = STATE_PLAYING
|
|
220
237
|
else:
|
|
221
238
|
self._unset_media_state()
|
|
@@ -255,8 +272,11 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
255
272
|
|
|
256
273
|
def _update_netaudio(self) -> None:
|
|
257
274
|
"""Update netaudio information."""
|
|
258
|
-
self._device.
|
|
259
|
-
|
|
275
|
+
if self._device.telnet_available:
|
|
276
|
+
self._device.telnet_api.send_commands("NSE")
|
|
277
|
+
self._schedule_netaudio_update()
|
|
278
|
+
else:
|
|
279
|
+
self._stop_media_update()
|
|
260
280
|
|
|
261
281
|
async def _async_netaudio_callback(
|
|
262
282
|
self, zone: str, event: str, parameter: str
|
|
@@ -267,12 +287,12 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
267
287
|
if self._input_func not in self._netaudio_func_list:
|
|
268
288
|
return
|
|
269
289
|
|
|
270
|
-
if parameter.startswith("
|
|
271
|
-
self._title = parameter[
|
|
272
|
-
elif parameter.startswith("
|
|
273
|
-
self._artist = parameter[
|
|
274
|
-
elif parameter.startswith("
|
|
275
|
-
self._album = parameter[
|
|
290
|
+
if parameter.startswith("1"):
|
|
291
|
+
self._title = parameter[1:]
|
|
292
|
+
elif parameter.startswith("2"):
|
|
293
|
+
self._artist = parameter[1:]
|
|
294
|
+
elif parameter.startswith("4"):
|
|
295
|
+
self._album = parameter[1:]
|
|
276
296
|
self._band = None
|
|
277
297
|
self._frequency = None
|
|
278
298
|
self._station = None
|
|
@@ -283,7 +303,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
283
303
|
port=self._device.api.port,
|
|
284
304
|
hash=hash((self._title, self._artist, self._album)),
|
|
285
305
|
)
|
|
286
|
-
await self.
|
|
306
|
+
await self._async_test_image_accessible()
|
|
287
307
|
|
|
288
308
|
def _schedule_tuner_update(self) -> None:
|
|
289
309
|
"""Schedule a tuner update task."""
|
|
@@ -295,8 +315,11 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
295
315
|
|
|
296
316
|
def _update_tuner(self) -> None:
|
|
297
317
|
"""Update tuner information."""
|
|
298
|
-
self._device.
|
|
299
|
-
|
|
318
|
+
if self._device.telnet_available:
|
|
319
|
+
self._device.telnet_api.send_commands("TFAN?", "TFANNAME?")
|
|
320
|
+
self._schedule_tuner_update()
|
|
321
|
+
else:
|
|
322
|
+
self._stop_media_update()
|
|
300
323
|
|
|
301
324
|
async def _async_tuner_callback(
|
|
302
325
|
self, zone: str, event: str, parameter: str
|
|
@@ -308,9 +331,9 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
308
331
|
return
|
|
309
332
|
|
|
310
333
|
if parameter.startswith("ANNAME"):
|
|
311
|
-
self._station = parameter[6:]
|
|
334
|
+
self._station = parameter[6:]
|
|
312
335
|
elif len(parameter) == 8:
|
|
313
|
-
self._frequency = "{
|
|
336
|
+
self._frequency = f"{parameter[2:6]}.{parameter[6:]}".strip("0")
|
|
314
337
|
if parameter[2:] > "050000":
|
|
315
338
|
self._band = "AM"
|
|
316
339
|
else:
|
|
@@ -324,7 +347,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
324
347
|
self._image_url = STATIC_ALBUM_URL.format(
|
|
325
348
|
host=self._device.api.host, port=self._device.api.port
|
|
326
349
|
)
|
|
327
|
-
await self.
|
|
350
|
+
await self._async_test_image_accessible()
|
|
328
351
|
|
|
329
352
|
def _schedule_hdtuner_update(self) -> None:
|
|
330
353
|
"""Schedule a HD tuner update task."""
|
|
@@ -336,8 +359,11 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
336
359
|
|
|
337
360
|
def _update_hdtuner(self) -> None:
|
|
338
361
|
"""Update HD tuner information."""
|
|
339
|
-
self._device.
|
|
340
|
-
|
|
362
|
+
if self._device.telnet_available:
|
|
363
|
+
self._device.telnet_api.send_commands("HD?")
|
|
364
|
+
self._schedule_hdtuner_update()
|
|
365
|
+
else:
|
|
366
|
+
self._stop_media_update()
|
|
341
367
|
|
|
342
368
|
async def _async_hdtuner_callback(
|
|
343
369
|
self, zone: str, event: str, parameter: str
|
|
@@ -349,13 +375,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
349
375
|
return
|
|
350
376
|
|
|
351
377
|
if parameter.startswith("ARTIST"):
|
|
352
|
-
self._artist = parameter[6:]
|
|
378
|
+
self._artist = parameter[6:]
|
|
353
379
|
elif parameter.startswith("TITLE"):
|
|
354
|
-
self._title = parameter[5:]
|
|
380
|
+
self._title = parameter[5:]
|
|
355
381
|
elif parameter.startswith("ALBUM"):
|
|
356
|
-
self._album = parameter[5:]
|
|
382
|
+
self._album = parameter[5:]
|
|
357
383
|
elif parameter.startswith("ST NAME"):
|
|
358
|
-
self._station = parameter[7:]
|
|
384
|
+
self._station = parameter[7:]
|
|
359
385
|
|
|
360
386
|
self._band = None
|
|
361
387
|
self._frequency = None
|
|
@@ -364,13 +390,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
364
390
|
self._image_url = STATIC_ALBUM_URL.format(
|
|
365
391
|
host=self._device.api.host, port=self._device.api.port
|
|
366
392
|
)
|
|
367
|
-
await self.
|
|
393
|
+
await self._async_test_image_accessible()
|
|
368
394
|
|
|
369
395
|
async def _async_input_func_update_callback(
|
|
370
396
|
self, zone: str, event: str, parameter: str
|
|
371
397
|
) -> None:
|
|
372
398
|
"""Handle input func update events."""
|
|
373
|
-
if self._input_func_update_lock.locked()
|
|
399
|
+
if self._input_func_update_lock.locked():
|
|
374
400
|
return
|
|
375
401
|
async with self._input_func_update_lock:
|
|
376
402
|
await self.async_update_inputfuncs()
|
|
@@ -380,7 +406,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
380
406
|
) -> None:
|
|
381
407
|
"""Update input functions asynchronously."""
|
|
382
408
|
# Ensure instance is setup before updating
|
|
383
|
-
if self._is_setup
|
|
409
|
+
if not self._is_setup:
|
|
384
410
|
self.setup()
|
|
385
411
|
|
|
386
412
|
# Update input functions
|
|
@@ -425,9 +451,9 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
425
451
|
# Get list of all input sources of receiver
|
|
426
452
|
xml_list = xml_zonecapa.find("./InputSource/List")
|
|
427
453
|
for xml_source in xml_list.findall("Source"):
|
|
428
|
-
receiver_sources[
|
|
429
|
-
xml_source.find("
|
|
430
|
-
|
|
454
|
+
receiver_sources[xml_source.find("FuncName").text] = (
|
|
455
|
+
xml_source.find("DefaultName").text
|
|
456
|
+
)
|
|
431
457
|
|
|
432
458
|
return receiver_sources
|
|
433
459
|
|
|
@@ -462,9 +488,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
462
488
|
raise
|
|
463
489
|
|
|
464
490
|
for child in xml.findall(
|
|
465
|
-
"./cmd[@{
|
|
466
|
-
|
|
467
|
-
)
|
|
491
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{AppCommands.GetRenameSource.cmd_text}']"
|
|
492
|
+
"/functionrename/list"
|
|
468
493
|
):
|
|
469
494
|
try:
|
|
470
495
|
renamed_sources[child.find("name").text.strip()] = child.find(
|
|
@@ -474,9 +499,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
474
499
|
continue
|
|
475
500
|
|
|
476
501
|
for child in xml.findall(
|
|
477
|
-
"./cmd[@{
|
|
478
|
-
|
|
479
|
-
)
|
|
502
|
+
f"./cmd[@{APPCOMMAND_CMD_TEXT}='{AppCommands.GetDeletedSource.cmd_text}']"
|
|
503
|
+
"/functiondelete/list"
|
|
480
504
|
):
|
|
481
505
|
try:
|
|
482
506
|
deleted_sources[child.find("FuncName").text.strip()] = (
|
|
@@ -519,9 +543,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
519
543
|
)
|
|
520
544
|
else:
|
|
521
545
|
raise AvrProcessingError(
|
|
522
|
-
"Method does not work for receiver type
|
|
523
|
-
|
|
524
|
-
)
|
|
546
|
+
"Method does not work for receiver type"
|
|
547
|
+
f" {self._device.receiver.type}"
|
|
525
548
|
)
|
|
526
549
|
except AvrRequestError as err:
|
|
527
550
|
_LOGGER.debug("Error when getting changed sources", exc_info=err)
|
|
@@ -608,9 +631,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
608
631
|
"Receiver sources list empty. Please check if device is powered on."
|
|
609
632
|
)
|
|
610
633
|
|
|
634
|
+
# Add additional input functions discovered at run-time
|
|
635
|
+
for function in self._additional_input_funcs:
|
|
636
|
+
receiver_sources[function] = function
|
|
637
|
+
|
|
611
638
|
# Get renamed and deleted sources
|
|
612
639
|
# From Appcommand.xml if supported
|
|
613
|
-
if self._device.use_avr_2016_update
|
|
640
|
+
if self._device.use_avr_2016_update:
|
|
614
641
|
(
|
|
615
642
|
renamed_sources,
|
|
616
643
|
deleted_sources,
|
|
@@ -630,7 +657,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
630
657
|
receiver_sources[key] = value
|
|
631
658
|
|
|
632
659
|
# Remove all deleted sources
|
|
633
|
-
if self._show_all_inputs
|
|
660
|
+
if not self._show_all_inputs:
|
|
634
661
|
for deleted_source in deleted_sources.items():
|
|
635
662
|
if deleted_source[1] == "DEL":
|
|
636
663
|
receiver_sources.pop(deleted_source[0], None)
|
|
@@ -642,7 +669,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
642
669
|
playing_func_list = []
|
|
643
670
|
|
|
644
671
|
for item in receiver_sources.items():
|
|
645
|
-
# Mapping of item[0] because some func names are
|
|
672
|
+
# Mapping of item[0] because some func names are inconsistent
|
|
646
673
|
# at AVR-X receivers
|
|
647
674
|
|
|
648
675
|
m_item_0 = SOURCE_MAPPING.get(item[0], item[0])
|
|
@@ -689,8 +716,12 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
689
716
|
deleted_sources,
|
|
690
717
|
) = await self.async_get_changed_sources_status_xml(cache_id=cache_id)
|
|
691
718
|
|
|
719
|
+
# Add additional input functions discovered at run-time
|
|
720
|
+
for function in self._additional_input_funcs:
|
|
721
|
+
receiver_sources[function] = function
|
|
722
|
+
|
|
692
723
|
# Remove all deleted sources
|
|
693
|
-
if self._show_all_inputs
|
|
724
|
+
if not self._show_all_inputs:
|
|
694
725
|
for deleted_source in deleted_sources.items():
|
|
695
726
|
if deleted_source[1] == "DEL":
|
|
696
727
|
receiver_sources.pop(deleted_source[0], None)
|
|
@@ -719,21 +750,22 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
719
750
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
720
751
|
):
|
|
721
752
|
"""Update state of device."""
|
|
722
|
-
if self._device.use_avr_2016_update is
|
|
753
|
+
if self._device.use_avr_2016_update is None:
|
|
754
|
+
raise AvrProcessingError(
|
|
755
|
+
"Device is not setup correctly, update method not set"
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
if self._device.use_avr_2016_update:
|
|
723
759
|
await self.async_update_attrs_appcommand(
|
|
724
760
|
self.appcommand_attrs, global_update=global_update, cache_id=cache_id
|
|
725
761
|
)
|
|
726
|
-
|
|
762
|
+
else:
|
|
727
763
|
urls = [self._device.urls.status]
|
|
728
764
|
if self._device.zone == MAIN_ZONE:
|
|
729
765
|
urls.append(self._device.urls.mainzone)
|
|
730
766
|
await self.async_update_attrs_status_xml(
|
|
731
767
|
self.status_xml_attrs, urls, cache_id=cache_id
|
|
732
768
|
)
|
|
733
|
-
else:
|
|
734
|
-
raise AvrProcessingError(
|
|
735
|
-
"Device is not setup correctly, update method not set"
|
|
736
|
-
)
|
|
737
769
|
|
|
738
770
|
async def async_update_media_state(self, cache_id: Optional[Hashable] = None):
|
|
739
771
|
"""Update media state of device."""
|
|
@@ -815,10 +847,10 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
815
847
|
# On track change assume device is PLAYING
|
|
816
848
|
self._state = STATE_PLAYING
|
|
817
849
|
|
|
818
|
-
await self.
|
|
850
|
+
await self._async_test_image_accessible()
|
|
819
851
|
|
|
820
|
-
async def
|
|
821
|
-
"""Test if image URL is
|
|
852
|
+
async def _async_test_image_accessible(self) -> None:
|
|
853
|
+
"""Test if image URL is accessible."""
|
|
822
854
|
if self._image_available is None and self._image_url is not None:
|
|
823
855
|
client = self._device.api.async_client_getter()
|
|
824
856
|
try:
|
|
@@ -842,7 +874,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
842
874
|
if self._device.api.is_default_async_client():
|
|
843
875
|
await client.aclose()
|
|
844
876
|
# Already tested that image URL is not accessible
|
|
845
|
-
elif self._image_available
|
|
877
|
+
elif not self._image_available:
|
|
846
878
|
self._image_url = None
|
|
847
879
|
|
|
848
880
|
def _unset_media_state(self) -> None:
|
|
@@ -959,20 +991,24 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
959
991
|
direct_mapping = False
|
|
960
992
|
# AVR-nonX receiver and if no mapping was found get parameter for
|
|
961
993
|
# setting input_func directly
|
|
962
|
-
if direct_mapping
|
|
994
|
+
if direct_mapping:
|
|
963
995
|
try:
|
|
964
996
|
linp = self._input_func_map[input_func]
|
|
965
997
|
except KeyError as err:
|
|
966
998
|
raise AvrCommandError(
|
|
967
|
-
"No mapping for input source {}"
|
|
999
|
+
f"No mapping for input source {input_func}"
|
|
968
1000
|
) from err
|
|
969
1001
|
# Create command URL and send command via HTTP GET
|
|
970
1002
|
if linp in self._favorite_func_list:
|
|
971
1003
|
command_url = self._device.urls.command_fav_src + linp
|
|
1004
|
+
telnet_command = self._device.telnet_commands.command_fav_src + linp
|
|
972
1005
|
else:
|
|
973
1006
|
command_url = self._device.urls.command_sel_src + linp
|
|
974
|
-
|
|
975
|
-
|
|
1007
|
+
telnet_command = self._device.telnet_commands.command_sel_src + linp
|
|
1008
|
+
if self._device.telnet_available:
|
|
1009
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
1010
|
+
else:
|
|
1011
|
+
await self._device.api.async_get_command(command_url)
|
|
976
1012
|
|
|
977
1013
|
async def async_toggle_play_pause(self) -> None:
|
|
978
1014
|
"""Toggle play pause media player."""
|
|
@@ -993,15 +1029,26 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
993
1029
|
if self._state == STATE_PLAYING:
|
|
994
1030
|
_LOGGER.info("Already playing, play command not sent")
|
|
995
1031
|
return
|
|
996
|
-
|
|
997
|
-
|
|
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:
|
|
1037
|
+
await self._device.api.async_get_command(self._device.urls.command_play)
|
|
998
1038
|
self._state = STATE_PLAYING
|
|
999
1039
|
|
|
1000
1040
|
async def async_pause(self) -> None:
|
|
1001
1041
|
"""Send pause command to receiver command via HTTP post."""
|
|
1002
1042
|
# Use pause command only for sources which support NETAUDIO
|
|
1003
1043
|
if self._input_func in self._netaudio_func_list:
|
|
1004
|
-
|
|
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:
|
|
1049
|
+
await self._device.api.async_get_command(
|
|
1050
|
+
self._device.urls.command_pause
|
|
1051
|
+
)
|
|
1005
1052
|
self._state = STATE_PAUSED
|
|
1006
1053
|
|
|
1007
1054
|
async def async_previous_track(self) -> None:
|
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__)
|
|
@@ -43,7 +44,7 @@ def sound_mode_map_factory() -> Dict[str, List]:
|
|
|
43
44
|
return sound_mode_map
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def sound_mode_rev_map_factory(instance:
|
|
47
|
+
def sound_mode_rev_map_factory(instance: "DenonAVRSoundMode") -> Dict[str, str]:
|
|
47
48
|
"""
|
|
48
49
|
Construct the sound_mode_rev_map.
|
|
49
50
|
|
|
@@ -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:
|
|
@@ -135,7 +136,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
135
136
|
) -> None:
|
|
136
137
|
"""Update sound mode asynchronously."""
|
|
137
138
|
# Ensure instance is setup before updating
|
|
138
|
-
if self._is_setup
|
|
139
|
+
if not self._is_setup:
|
|
139
140
|
await self.async_setup()
|
|
140
141
|
|
|
141
142
|
# Update state
|
|
@@ -147,13 +148,18 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
147
148
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
148
149
|
):
|
|
149
150
|
"""Update sound mode status of device."""
|
|
150
|
-
if self._device.use_avr_2016_update is
|
|
151
|
+
if self._device.use_avr_2016_update is None:
|
|
152
|
+
raise AvrProcessingError(
|
|
153
|
+
"Device is not setup correctly, update method not set"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if self._device.use_avr_2016_update:
|
|
151
157
|
await self.async_update_attrs_appcommand(
|
|
152
158
|
self.appcommand_attrs, global_update=global_update, cache_id=cache_id
|
|
153
159
|
)
|
|
154
|
-
|
|
160
|
+
else:
|
|
155
161
|
urls = [self._device.urls.status, self._device.urls.mainzone]
|
|
156
|
-
if self.
|
|
162
|
+
if self._is_setup and not self._support_sound_mode:
|
|
157
163
|
return
|
|
158
164
|
# There are two different options of sound mode tags
|
|
159
165
|
try:
|
|
@@ -170,10 +176,6 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
170
176
|
self._support_sound_mode = False
|
|
171
177
|
return
|
|
172
178
|
self._support_sound_mode = True
|
|
173
|
-
else:
|
|
174
|
-
raise AvrProcessingError(
|
|
175
|
-
"Device is not setup correctly, update method not set"
|
|
176
|
-
)
|
|
177
179
|
|
|
178
180
|
def match_sound_mode(self) -> Optional[str]:
|
|
179
181
|
"""Match the raw_sound_mode to its corresponding sound_mode."""
|
|
@@ -229,12 +231,17 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
229
231
|
Calls command to activate/deactivate the mode
|
|
230
232
|
"""
|
|
231
233
|
command_url = self._device.urls.command_set_all_zone_stereo
|
|
234
|
+
telnet_command = self._device.telnet_commands.command_set_all_zone_stereo
|
|
232
235
|
if zst_on:
|
|
233
236
|
command_url += "ZST ON"
|
|
237
|
+
telnet_command += "ZST ON"
|
|
234
238
|
else:
|
|
235
239
|
command_url += "ZST OFF"
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
telnet_command += "ZST OFF"
|
|
241
|
+
if self._device.telnet_available:
|
|
242
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
243
|
+
else:
|
|
244
|
+
await self._device.api.async_get_command(command_url)
|
|
238
245
|
|
|
239
246
|
##############
|
|
240
247
|
# Properties #
|
|
@@ -251,7 +258,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
251
258
|
return sound_mode_matched
|
|
252
259
|
|
|
253
260
|
@property
|
|
254
|
-
def sound_mode_list(self) ->
|
|
261
|
+
def sound_mode_list(self) -> List[str]:
|
|
255
262
|
"""Return a list of available sound modes as string."""
|
|
256
263
|
return list(self._sound_mode_map.keys())
|
|
257
264
|
|
|
@@ -280,6 +287,9 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
280
287
|
Valid values depend on the device and should be taken from
|
|
281
288
|
"sound_mode_list".
|
|
282
289
|
"""
|
|
290
|
+
if sound_mode not in self.sound_mode_list:
|
|
291
|
+
raise AvrCommandError(f"{sound_mode} is not a valid sound mode")
|
|
292
|
+
|
|
283
293
|
if sound_mode == ALL_ZONE_STEREO:
|
|
284
294
|
await self._async_set_all_zone_stereo(True)
|
|
285
295
|
return
|
|
@@ -291,8 +301,14 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
291
301
|
# Therefore source mapping is needed to get sound_mode
|
|
292
302
|
# Create command URL and send command via HTTP GET
|
|
293
303
|
command_url = self._device.urls.command_sel_sound_mode + sound_mode
|
|
304
|
+
telnet_command = (
|
|
305
|
+
self._device.telnet_commands.command_sel_sound_mode + sound_mode
|
|
306
|
+
)
|
|
294
307
|
# sent command
|
|
295
|
-
|
|
308
|
+
if self._device.telnet_available:
|
|
309
|
+
await self._device.telnet_api.async_send_commands(telnet_command)
|
|
310
|
+
else:
|
|
311
|
+
await self._device.api.async_get_command(command_url)
|
|
296
312
|
|
|
297
313
|
|
|
298
314
|
def sound_mode_factory(instance: DenonAVRFoundation) -> DenonAVRSoundMode:
|
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
|