denonavr 0.11.6__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- denonavr/__init__.py +1 -24
- denonavr/api.py +181 -94
- denonavr/audyssey.py +12 -7
- denonavr/const.py +168 -59
- denonavr/decorators.py +9 -76
- denonavr/denonavr.py +47 -187
- denonavr/foundation.py +126 -40
- denonavr/input.py +57 -9
- denonavr/soundmode.py +51 -36
- denonavr/tonecontrol.py +47 -12
- denonavr/volume.py +4 -2
- denonavr-1.0.1.dist-info/METADATA +158 -0
- denonavr-1.0.1.dist-info/RECORD +19 -0
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/WHEEL +1 -1
- denonavr-0.11.6.dist-info/METADATA +0 -290
- denonavr-0.11.6.dist-info/RECORD +0 -19
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/LICENSE +0 -0
- {denonavr-0.11.6.dist-info → denonavr-1.0.1.dist-info}/top_level.txt +0 -0
denonavr/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
|
|
@@ -143,16 +142,19 @@ class DenonAVRDeviceInfo:
|
|
|
143
142
|
async def async_setup(self) -> None:
|
|
144
143
|
"""Ensure that configuration is loaded from receiver asynchronously."""
|
|
145
144
|
async with self._setup_lock:
|
|
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:
|
|
151
|
+
_LOGGER.debug("Identifying receiver")
|
|
152
152
|
await self.async_identify_receiver()
|
|
153
|
+
_LOGGER.debug("Getting device info")
|
|
153
154
|
await self.async_get_device_info()
|
|
154
155
|
finally:
|
|
155
|
-
self.api.
|
|
156
|
+
self.api.read_timeout = read_timeout
|
|
157
|
+
_LOGGER.debug("Identifying update method")
|
|
156
158
|
await self.async_identify_update_method()
|
|
157
159
|
|
|
158
160
|
# Add tags for a potential AppCommand.xml update
|
|
@@ -166,17 +168,20 @@ class DenonAVRDeviceInfo:
|
|
|
166
168
|
self.telnet_api.register_callback(power_event, self._async_power_callback)
|
|
167
169
|
|
|
168
170
|
self._is_setup = True
|
|
171
|
+
_LOGGER.debug("Finished device setup")
|
|
169
172
|
|
|
170
173
|
async def async_update(
|
|
171
174
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
172
175
|
) -> None:
|
|
173
176
|
"""Update status asynchronously."""
|
|
177
|
+
_LOGGER.debug("Starting device update")
|
|
174
178
|
# Ensure instance is setup before updating
|
|
175
179
|
if not self._is_setup:
|
|
176
180
|
await self.async_setup()
|
|
177
181
|
|
|
178
182
|
# Update power status
|
|
179
183
|
await self.async_update_power(global_update=global_update, cache_id=cache_id)
|
|
184
|
+
_LOGGER.debug("Finished device update")
|
|
180
185
|
|
|
181
186
|
async def async_identify_receiver(self) -> None:
|
|
182
187
|
"""Identify receiver asynchronously."""
|
|
@@ -196,9 +201,9 @@ class DenonAVRDeviceInfo:
|
|
|
196
201
|
)
|
|
197
202
|
except (AvrTimoutError, AvrNetworkError) as err:
|
|
198
203
|
_LOGGER.debug(
|
|
199
|
-
"Connection error on port %s when identifying receiver",
|
|
204
|
+
"Connection error on port %s when identifying receiver: %s",
|
|
200
205
|
r_type.port,
|
|
201
|
-
|
|
206
|
+
err,
|
|
202
207
|
)
|
|
203
208
|
|
|
204
209
|
# Raise error only when occurred at both types
|
|
@@ -210,22 +215,32 @@ class DenonAVRDeviceInfo:
|
|
|
210
215
|
_LOGGER.debug(
|
|
211
216
|
(
|
|
212
217
|
"Request error on port %s when identifying receiver, "
|
|
213
|
-
"device is not a %s
|
|
218
|
+
"device is not a %s receiver: %s"
|
|
214
219
|
),
|
|
215
220
|
r_type.port,
|
|
216
221
|
r_type.type,
|
|
217
|
-
|
|
222
|
+
err,
|
|
218
223
|
)
|
|
219
224
|
else:
|
|
220
225
|
is_avr_x = self._is_avr_x(xml)
|
|
221
226
|
if is_avr_x:
|
|
222
227
|
self.receiver = r_type
|
|
228
|
+
_LOGGER.info(
|
|
229
|
+
"Identified %s receiver using port %s",
|
|
230
|
+
r_type.type,
|
|
231
|
+
r_type.port,
|
|
232
|
+
)
|
|
223
233
|
# Receiver identified, return
|
|
224
234
|
return
|
|
225
235
|
|
|
226
236
|
# If check of Deviceinfo.xml was not successful, receiver is type AVR
|
|
227
237
|
self.receiver = AVR
|
|
228
238
|
self.api.port = AVR.port
|
|
239
|
+
_LOGGER.info(
|
|
240
|
+
"Identified %s receiver using port %s",
|
|
241
|
+
AVR.type,
|
|
242
|
+
AVR.port,
|
|
243
|
+
)
|
|
229
244
|
|
|
230
245
|
@staticmethod
|
|
231
246
|
def _is_avr_x(deviceinfo: ET.Element) -> bool:
|
|
@@ -275,13 +290,11 @@ class DenonAVRDeviceInfo:
|
|
|
275
290
|
)
|
|
276
291
|
except (AvrTimoutError, AvrNetworkError) as err:
|
|
277
292
|
_LOGGER.debug(
|
|
278
|
-
"Connection error when identifying update method",
|
|
293
|
+
"Connection error when identifying update method: %s", err
|
|
279
294
|
)
|
|
280
295
|
raise
|
|
281
296
|
except AvrRequestError as err:
|
|
282
|
-
_LOGGER.debug(
|
|
283
|
-
"Request error when identifying update method", exc_info=err
|
|
284
|
-
)
|
|
297
|
+
_LOGGER.debug("Request error when identifying update method: %s", err)
|
|
285
298
|
self.use_avr_2016_update = False
|
|
286
299
|
_LOGGER.info("AVR-X device, AppCommand.xml interface not supported")
|
|
287
300
|
else:
|
|
@@ -294,11 +307,11 @@ class DenonAVRDeviceInfo:
|
|
|
294
307
|
xml = await self.api.async_get_xml(self.urls.mainzone)
|
|
295
308
|
except (AvrTimoutError, AvrNetworkError) as err:
|
|
296
309
|
_LOGGER.debug(
|
|
297
|
-
"Connection error when identifying update method",
|
|
310
|
+
"Connection error when identifying update method: %s", err
|
|
298
311
|
)
|
|
299
312
|
raise
|
|
300
313
|
except AvrRequestError as err:
|
|
301
|
-
_LOGGER.debug("Request error getting friendly name",
|
|
314
|
+
_LOGGER.debug("Request error getting friendly name: %s", err)
|
|
302
315
|
_LOGGER.info(
|
|
303
316
|
"Receiver name could not be determined. Using standard"
|
|
304
317
|
" name: Denon AVR"
|
|
@@ -309,7 +322,7 @@ class DenonAVRDeviceInfo:
|
|
|
309
322
|
self._set_friendly_name(xml)
|
|
310
323
|
|
|
311
324
|
async def async_verify_avr_2016_update_method(
|
|
312
|
-
self, cache_id: Hashable = None
|
|
325
|
+
self, *, cache_id: Hashable = None
|
|
313
326
|
) -> None:
|
|
314
327
|
"""Verify if avr 2016 update method is working."""
|
|
315
328
|
# Nothing to do if Appcommand.xml interface is not supported
|
|
@@ -320,7 +333,7 @@ class DenonAVRDeviceInfo:
|
|
|
320
333
|
# Result is cached that it can be reused during update
|
|
321
334
|
await self.api.async_get_global_appcommand(cache_id=cache_id)
|
|
322
335
|
except (AvrTimoutError, AvrNetworkError) as err:
|
|
323
|
-
_LOGGER.debug("Connection error when verifying update method",
|
|
336
|
+
_LOGGER.debug("Connection error when verifying update method: %s", err)
|
|
324
337
|
raise
|
|
325
338
|
except AvrForbiddenError:
|
|
326
339
|
# Recovery in case receiver changes port from 80 to 8080 which
|
|
@@ -338,7 +351,7 @@ class DenonAVRDeviceInfo:
|
|
|
338
351
|
else:
|
|
339
352
|
raise
|
|
340
353
|
except AvrIncompleteResponseError as err:
|
|
341
|
-
_LOGGER.debug("Request error when verifying update method",
|
|
354
|
+
_LOGGER.debug("Request error when verifying update method: %s", err)
|
|
342
355
|
# Only AVR_X devices support both interfaces
|
|
343
356
|
if self.receiver == AVR_X:
|
|
344
357
|
_LOGGER.warning(
|
|
@@ -378,10 +391,10 @@ class DenonAVRDeviceInfo:
|
|
|
378
391
|
try:
|
|
379
392
|
res = await self.api.async_get(command, port=port)
|
|
380
393
|
except AvrTimoutError as err:
|
|
381
|
-
_LOGGER.debug("Timeout when getting device info",
|
|
394
|
+
_LOGGER.debug("Timeout when getting device info: %s", err)
|
|
382
395
|
raise
|
|
383
396
|
except AvrNetworkError as err:
|
|
384
|
-
_LOGGER.debug("Network error getting device info",
|
|
397
|
+
_LOGGER.debug("Network error getting device info: %s", err)
|
|
385
398
|
raise
|
|
386
399
|
except AvrRequestError as err:
|
|
387
400
|
_LOGGER.error(
|
|
@@ -444,7 +457,7 @@ class DenonAVRDeviceInfo:
|
|
|
444
457
|
self.urls.appcommand, tuple(power_appcommand), cache_id=cache_id
|
|
445
458
|
)
|
|
446
459
|
except AvrRequestError as err:
|
|
447
|
-
_LOGGER.debug("Error when getting power status",
|
|
460
|
+
_LOGGER.debug("Error when getting power status: %s", err)
|
|
448
461
|
raise
|
|
449
462
|
|
|
450
463
|
# Extract relevant information
|
|
@@ -482,7 +495,7 @@ class DenonAVRDeviceInfo:
|
|
|
482
495
|
xml = await self.api.async_get_xml(url, cache_id=cache_id)
|
|
483
496
|
except AvrRequestError as err:
|
|
484
497
|
_LOGGER.debug(
|
|
485
|
-
"Error when getting power status from url %s", url,
|
|
498
|
+
"Error when getting power status from url %s: %s", url, err
|
|
486
499
|
)
|
|
487
500
|
continue
|
|
488
501
|
|
|
@@ -535,6 +548,87 @@ class DenonAVRDeviceInfo:
|
|
|
535
548
|
else:
|
|
536
549
|
await self.api.async_get_command(self.urls.command_power_standby)
|
|
537
550
|
|
|
551
|
+
async def async_cursor_up(self) -> None:
|
|
552
|
+
"""Cursor Up on receiver via HTTP get command."""
|
|
553
|
+
if self.telnet_available:
|
|
554
|
+
await self.telnet_api.async_send_commands(
|
|
555
|
+
self.telnet_commands.command_cusor_up
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
await self.api.async_get_command(self.urls.command_cusor_up)
|
|
559
|
+
|
|
560
|
+
async def async_cursor_down(self) -> None:
|
|
561
|
+
"""Cursor Down on receiver via HTTP get command."""
|
|
562
|
+
if self.telnet_available:
|
|
563
|
+
await self.telnet_api.async_send_commands(
|
|
564
|
+
self.telnet_commands.command_cusor_down
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
await self.api.async_get_command(self.urls.command_cusor_down)
|
|
568
|
+
|
|
569
|
+
async def async_cursor_left(self) -> None:
|
|
570
|
+
"""Cursor Left on receiver via HTTP get command."""
|
|
571
|
+
if self.telnet_available:
|
|
572
|
+
await self.telnet_api.async_send_commands(
|
|
573
|
+
self.telnet_commands.command_cusor_left
|
|
574
|
+
)
|
|
575
|
+
else:
|
|
576
|
+
await self.api.async_get_command(self.urls.command_cusor_left)
|
|
577
|
+
|
|
578
|
+
async def async_cursor_right(self) -> None:
|
|
579
|
+
"""Cursor Right on receiver via HTTP get command."""
|
|
580
|
+
if self.telnet_available:
|
|
581
|
+
await self.telnet_api.async_send_commands(
|
|
582
|
+
self.telnet_commands.command_cusor_right
|
|
583
|
+
)
|
|
584
|
+
else:
|
|
585
|
+
await self.api.async_get_command(self.urls.command_cusor_right)
|
|
586
|
+
|
|
587
|
+
async def async_cursor_enter(self) -> None:
|
|
588
|
+
"""Cursor Enter on receiver via HTTP get command."""
|
|
589
|
+
if self.telnet_available:
|
|
590
|
+
await self.telnet_api.async_send_commands(
|
|
591
|
+
self.telnet_commands.command_cusor_enter
|
|
592
|
+
)
|
|
593
|
+
else:
|
|
594
|
+
await self.api.async_get_command(self.urls.command_cusor_enter)
|
|
595
|
+
|
|
596
|
+
async def async_back(self) -> None:
|
|
597
|
+
"""Back command on receiver via HTTP get command."""
|
|
598
|
+
if self.telnet_available:
|
|
599
|
+
await self.telnet_api.async_send_commands(self.telnet_commands.command_back)
|
|
600
|
+
else:
|
|
601
|
+
await self.api.async_get_command(self.urls.command_back)
|
|
602
|
+
|
|
603
|
+
async def async_info(self) -> None:
|
|
604
|
+
"""Info OSD on receiver via HTTP get command."""
|
|
605
|
+
if self.telnet_available:
|
|
606
|
+
await self.telnet_api.async_send_commands(self.telnet_commands.command_info)
|
|
607
|
+
else:
|
|
608
|
+
await self.api.async_get_command(self.urls.command_info)
|
|
609
|
+
|
|
610
|
+
async def async_options(self) -> None:
|
|
611
|
+
"""Options menu on receiver via HTTP get command."""
|
|
612
|
+
await self.api.async_get_command(self.urls.command_options)
|
|
613
|
+
|
|
614
|
+
async def async_settings_menu(self) -> None:
|
|
615
|
+
"""Options menu on receiver via HTTP get command."""
|
|
616
|
+
res = await self.api.async_get_command(self.urls.command_setup_query)
|
|
617
|
+
if self.telnet_available:
|
|
618
|
+
if res is not None and res == "MNMEN ON":
|
|
619
|
+
await self.telnet_api.async_send_commands(
|
|
620
|
+
self.telnet_commands.command_setup_close
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
await self.telnet_api.async_send_commands(
|
|
624
|
+
self.telnet_commands.command_setup_open
|
|
625
|
+
)
|
|
626
|
+
else:
|
|
627
|
+
if res is not None and res == "MNMEN ON":
|
|
628
|
+
await self.api.async_get_command(self.urls.command_setup_close)
|
|
629
|
+
else:
|
|
630
|
+
await self.api.async_get_command(self.urls.command_setup_open)
|
|
631
|
+
|
|
538
632
|
|
|
539
633
|
@attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR)
|
|
540
634
|
class DenonAVRFoundation:
|
|
@@ -558,7 +652,6 @@ class DenonAVRFoundation:
|
|
|
558
652
|
appcommand0300: bool = False,
|
|
559
653
|
global_update: bool = False,
|
|
560
654
|
cache_id: Optional[Hashable] = None,
|
|
561
|
-
ignore_missing_response: bool = False,
|
|
562
655
|
):
|
|
563
656
|
"""Update attributes from AppCommand.xml."""
|
|
564
657
|
# Copy that we do not accidently change the wrong dict
|
|
@@ -581,7 +674,7 @@ class DenonAVRFoundation:
|
|
|
581
674
|
url, tags, cache_id=cache_id
|
|
582
675
|
)
|
|
583
676
|
except AvrRequestError as err:
|
|
584
|
-
_LOGGER.debug("Error when getting status update",
|
|
677
|
+
_LOGGER.debug("Error when getting status update: %s", err)
|
|
585
678
|
raise
|
|
586
679
|
|
|
587
680
|
# Extract relevant information
|
|
@@ -616,10 +709,10 @@ class DenonAVRFoundation:
|
|
|
616
709
|
|
|
617
710
|
except (AttributeError, IndexError) as err:
|
|
618
711
|
_LOGGER.debug(
|
|
619
|
-
"Failed updating attribute %s for zone %s",
|
|
712
|
+
"Failed updating attribute %s for zone %s: %s",
|
|
620
713
|
pattern.update_attribute,
|
|
621
714
|
self._device.zone,
|
|
622
|
-
|
|
715
|
+
err,
|
|
623
716
|
)
|
|
624
717
|
|
|
625
718
|
if start == success:
|
|
@@ -627,24 +720,17 @@ class DenonAVRFoundation:
|
|
|
627
720
|
update_attrs.pop(app_command, None)
|
|
628
721
|
|
|
629
722
|
# Check if each attribute was updated
|
|
630
|
-
if update_attrs
|
|
723
|
+
if update_attrs:
|
|
631
724
|
raise AvrProcessingError(
|
|
632
725
|
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
633
726
|
f" {update_attrs}"
|
|
634
727
|
)
|
|
635
|
-
if update_attrs and ignore_missing_response:
|
|
636
|
-
_LOGGER.debug(
|
|
637
|
-
"Some attributes of zone %s not found on update: %s",
|
|
638
|
-
self._device.zone,
|
|
639
|
-
update_attrs,
|
|
640
|
-
)
|
|
641
728
|
|
|
642
729
|
async def async_update_attrs_status_xml(
|
|
643
730
|
self,
|
|
644
731
|
update_attrs: Dict[str, str],
|
|
645
732
|
urls: List[str],
|
|
646
733
|
cache_id: Optional[Hashable] = None,
|
|
647
|
-
ignore_missing_response: bool = False,
|
|
648
734
|
):
|
|
649
735
|
"""
|
|
650
736
|
Update attributes from status xml.
|
|
@@ -664,7 +750,7 @@ class DenonAVRFoundation:
|
|
|
664
750
|
xml = await self._device.api.async_get_xml(url, cache_id=cache_id)
|
|
665
751
|
except AvrRequestError as err:
|
|
666
752
|
_LOGGER.debug(
|
|
667
|
-
"Error when getting status update from url %s", url,
|
|
753
|
+
"Error when getting status update from url %s: %s", url, err
|
|
668
754
|
)
|
|
669
755
|
continue
|
|
670
756
|
attrs = deepcopy(update_attrs)
|
|
@@ -683,10 +769,10 @@ class DenonAVRFoundation:
|
|
|
683
769
|
|
|
684
770
|
except (AttributeError, IndexError) as err:
|
|
685
771
|
_LOGGER.debug(
|
|
686
|
-
"Failed updating attribute %s for zone %s",
|
|
772
|
+
"Failed updating attribute %s for zone %s: %s",
|
|
687
773
|
name,
|
|
688
774
|
self._device.zone,
|
|
689
|
-
|
|
775
|
+
err,
|
|
690
776
|
)
|
|
691
777
|
|
|
692
778
|
# All done, no need for continuing
|
|
@@ -694,7 +780,7 @@ class DenonAVRFoundation:
|
|
|
694
780
|
break
|
|
695
781
|
|
|
696
782
|
# Check if each attribute was updated
|
|
697
|
-
if update_attrs
|
|
783
|
+
if update_attrs:
|
|
698
784
|
raise AvrProcessingError(
|
|
699
785
|
f"Some attributes of zone {self._device.zone} not found on update:"
|
|
700
786
|
f" {update_attrs}"
|
|
@@ -746,9 +832,9 @@ def set_api_timeout(
|
|
|
746
832
|
) -> float:
|
|
747
833
|
"""Change API timeout on timeout changes too."""
|
|
748
834
|
# First change _device.api.host then return value
|
|
749
|
-
timeout = httpx.Timeout(value, read=max(value, 15.0))
|
|
750
835
|
# pylint: disable=protected-access
|
|
751
|
-
instance._device.api.timeout =
|
|
836
|
+
instance._device.api.timeout = value
|
|
837
|
+
instance._device.api.read_timeout = max(value, 15.0)
|
|
752
838
|
instance._device.telnet_api.timeout = value
|
|
753
839
|
return value
|
|
754
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}
|
|
@@ -273,7 +280,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
273
280
|
def _update_netaudio(self) -> None:
|
|
274
281
|
"""Update netaudio information."""
|
|
275
282
|
if self._device.telnet_available:
|
|
276
|
-
self._device.telnet_api.send_commands("NSE")
|
|
283
|
+
self._device.telnet_api.send_commands("NSE", skip_confirmation=True)
|
|
277
284
|
self._schedule_netaudio_update()
|
|
278
285
|
else:
|
|
279
286
|
self._stop_media_update()
|
|
@@ -316,7 +323,9 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
316
323
|
def _update_tuner(self) -> None:
|
|
317
324
|
"""Update tuner information."""
|
|
318
325
|
if self._device.telnet_available:
|
|
319
|
-
self._device.telnet_api.send_commands(
|
|
326
|
+
self._device.telnet_api.send_commands(
|
|
327
|
+
"TFAN?", "TFANNAME?", skip_confirmation=True
|
|
328
|
+
)
|
|
320
329
|
self._schedule_tuner_update()
|
|
321
330
|
else:
|
|
322
331
|
self._stop_media_update()
|
|
@@ -360,7 +369,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
360
369
|
def _update_hdtuner(self) -> None:
|
|
361
370
|
"""Update HD tuner information."""
|
|
362
371
|
if self._device.telnet_available:
|
|
363
|
-
self._device.telnet_api.send_commands("HD?")
|
|
372
|
+
self._device.telnet_api.send_commands("HD?", skip_confirmation=True)
|
|
364
373
|
self._schedule_hdtuner_update()
|
|
365
374
|
else:
|
|
366
375
|
self._stop_media_update()
|
|
@@ -405,18 +414,23 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
405
414
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
406
415
|
) -> None:
|
|
407
416
|
"""Update input functions asynchronously."""
|
|
417
|
+
_LOGGER.debug("Starting input update")
|
|
408
418
|
# Ensure instance is setup before updating
|
|
409
419
|
if not self._is_setup:
|
|
410
420
|
self.setup()
|
|
411
421
|
|
|
412
422
|
# Update input functions
|
|
423
|
+
_LOGGER.debug("Updating input functions")
|
|
413
424
|
await self.async_update_inputfuncs(
|
|
414
425
|
global_update=global_update, cache_id=cache_id
|
|
415
426
|
)
|
|
416
427
|
# Update state
|
|
428
|
+
_LOGGER.debug("Updating input state")
|
|
417
429
|
await self.async_update_state(global_update=global_update, cache_id=cache_id)
|
|
418
430
|
# Update media state
|
|
431
|
+
_LOGGER.debug("Updating media state")
|
|
419
432
|
await self.async_update_media_state(cache_id=cache_id)
|
|
433
|
+
_LOGGER.debug("Finished input update")
|
|
420
434
|
|
|
421
435
|
async def async_get_sources_deviceinfo(self) -> Dict[str, str]:
|
|
422
436
|
"""Get sources from Deviceinfo.xml."""
|
|
@@ -426,7 +440,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
426
440
|
self._device.urls.deviceinfo, cache_id=id(self._device)
|
|
427
441
|
)
|
|
428
442
|
except AvrRequestError as err:
|
|
429
|
-
_LOGGER.debug("Error when getting sources",
|
|
443
|
+
_LOGGER.debug("Error when getting sources: %s", err)
|
|
430
444
|
raise
|
|
431
445
|
|
|
432
446
|
receiver_sources = {}
|
|
@@ -484,7 +498,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
484
498
|
self._device.urls.appcommand, tags, cache_id=cache_id
|
|
485
499
|
)
|
|
486
500
|
except AvrRequestError as err:
|
|
487
|
-
_LOGGER.debug("Error when getting changed sources",
|
|
501
|
+
_LOGGER.debug("Error when getting changed sources: %s", err)
|
|
488
502
|
raise
|
|
489
503
|
|
|
490
504
|
for child in xml.findall(
|
|
@@ -509,6 +523,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
509
523
|
except AttributeError:
|
|
510
524
|
continue
|
|
511
525
|
|
|
526
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
527
|
+
|
|
512
528
|
return (renamed_sources, deleted_sources)
|
|
513
529
|
|
|
514
530
|
async def async_get_changed_sources_status_xml(
|
|
@@ -547,7 +563,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
547
563
|
f" {self._device.receiver.type}"
|
|
548
564
|
)
|
|
549
565
|
except AvrRequestError as err:
|
|
550
|
-
_LOGGER.debug("Error when getting changed sources",
|
|
566
|
+
_LOGGER.debug("Error when getting changed sources: %s", err)
|
|
551
567
|
raise
|
|
552
568
|
|
|
553
569
|
# Get the relevant tags from XML structure
|
|
@@ -597,6 +613,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
597
613
|
except IndexError:
|
|
598
614
|
_LOGGER.error("List of deleted sources incomplete, continuing anyway")
|
|
599
615
|
|
|
616
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
617
|
+
|
|
600
618
|
return (renamed_sources, deleted_sources)
|
|
601
619
|
|
|
602
620
|
async def async_update_inputfuncs(
|
|
@@ -852,10 +870,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
852
870
|
async def _async_test_image_accessible(self) -> None:
|
|
853
871
|
"""Test if image URL is accessible."""
|
|
854
872
|
if self._image_available is None and self._image_url is not None:
|
|
855
|
-
client = self._device.api.
|
|
873
|
+
client = self._device.api.httpx_async_client.client_getter()
|
|
856
874
|
try:
|
|
857
875
|
res = await client.get(
|
|
858
|
-
self._image_url,
|
|
876
|
+
self._image_url,
|
|
877
|
+
timeout=httpx.Timeout(
|
|
878
|
+
self._device.api.timeout, read=self._device.api.read_timeout
|
|
879
|
+
),
|
|
859
880
|
)
|
|
860
881
|
res.raise_for_status()
|
|
861
882
|
except httpx.TimeoutException:
|
|
@@ -871,7 +892,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
871
892
|
self._image_available = True
|
|
872
893
|
finally:
|
|
873
894
|
# Close the default AsyncClient but keep custom clients open
|
|
874
|
-
if self._device.api.is_default_async_client():
|
|
895
|
+
if self._device.api.httpx_async_client.is_default_async_client():
|
|
875
896
|
await client.aclose()
|
|
876
897
|
# Already tested that image URL is not accessible
|
|
877
898
|
elif not self._image_available:
|
|
@@ -887,6 +908,33 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
887
908
|
self._station = None
|
|
888
909
|
self._image_url = None
|
|
889
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
|
+
|
|
890
938
|
##############
|
|
891
939
|
# Properties #
|
|
892
940
|
##############
|
denonavr/soundmode.py
CHANGED
|
@@ -16,14 +16,8 @@ from typing import Dict, List, Optional
|
|
|
16
16
|
import attr
|
|
17
17
|
|
|
18
18
|
from .appcommand import AppCommands
|
|
19
|
-
from .const import
|
|
20
|
-
|
|
21
|
-
AVR_X,
|
|
22
|
-
AVR_X_2016,
|
|
23
|
-
DENON_ATTR_SETATTR,
|
|
24
|
-
SOUND_MODE_MAPPING,
|
|
25
|
-
)
|
|
26
|
-
from .exceptions import AvrCommandError, AvrProcessingError
|
|
19
|
+
from .const import ALL_ZONE_STEREO, DENON_ATTR_SETATTR, SOUND_MODE_MAPPING
|
|
20
|
+
from .exceptions import AvrCommandError, AvrIncompleteResponseError, AvrProcessingError
|
|
27
21
|
from .foundation import DenonAVRFoundation
|
|
28
22
|
|
|
29
23
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -94,6 +88,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
94
88
|
init=False,
|
|
95
89
|
)
|
|
96
90
|
_setup_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
|
|
91
|
+
_appcommand_active: bool = attr.ib(converter=bool, default=True, init=False)
|
|
97
92
|
|
|
98
93
|
# Update tags for attributes
|
|
99
94
|
# AppCommand.xml interface
|
|
@@ -105,22 +100,22 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
105
100
|
async def async_setup(self) -> None:
|
|
106
101
|
"""Ensure that the instance is initialized."""
|
|
107
102
|
async with self._setup_lock:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self.
|
|
116
|
-
|
|
117
|
-
await self.async_update_sound_mode()
|
|
103
|
+
_LOGGER.debug("Starting sound mode setup")
|
|
104
|
+
|
|
105
|
+
# The first update determines if sound mode is supported
|
|
106
|
+
await self.async_update_sound_mode()
|
|
107
|
+
|
|
108
|
+
if self._support_sound_mode and self._appcommand_active:
|
|
109
|
+
# Add tags for a potential AppCommand.xml update
|
|
110
|
+
for tag in self.appcommand_attrs:
|
|
111
|
+
self._device.api.add_appcommand_update_tag(tag)
|
|
118
112
|
|
|
119
113
|
self._device.telnet_api.register_callback(
|
|
120
114
|
"MS", self._async_soundmode_callback
|
|
121
115
|
)
|
|
122
116
|
|
|
123
117
|
self._is_setup = True
|
|
118
|
+
_LOGGER.debug("Finished sound mode setup")
|
|
124
119
|
|
|
125
120
|
async def _async_soundmode_callback(
|
|
126
121
|
self, zone: str, event: str, parameter: str
|
|
@@ -135,6 +130,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
135
130
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
136
131
|
) -> None:
|
|
137
132
|
"""Update sound mode asynchronously."""
|
|
133
|
+
_LOGGER.debug("Starting sound mode update")
|
|
138
134
|
# Ensure instance is setup before updating
|
|
139
135
|
if not self._is_setup:
|
|
140
136
|
await self.async_setup()
|
|
@@ -143,6 +139,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
143
139
|
await self.async_update_sound_mode(
|
|
144
140
|
global_update=global_update, cache_id=cache_id
|
|
145
141
|
)
|
|
142
|
+
_LOGGER.debug("Finished sound mode update")
|
|
146
143
|
|
|
147
144
|
async def async_update_sound_mode(
|
|
148
145
|
self, global_update: bool = False, cache_id: Optional[Hashable] = None
|
|
@@ -153,29 +150,47 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
153
150
|
"Device is not setup correctly, update method not set"
|
|
154
151
|
)
|
|
155
152
|
|
|
156
|
-
if self.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
if self._is_setup and not self._support_sound_mode:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if self._device.use_avr_2016_update and self._appcommand_active:
|
|
157
|
+
try:
|
|
158
|
+
await self.async_update_attrs_appcommand(
|
|
159
|
+
self.appcommand_attrs,
|
|
160
|
+
global_update=global_update,
|
|
161
|
+
cache_id=cache_id,
|
|
162
|
+
)
|
|
163
|
+
except (AvrProcessingError, AvrIncompleteResponseError):
|
|
164
|
+
self._appcommand_active = False
|
|
165
|
+
_LOGGER.debug(
|
|
166
|
+
"Appcommand.xml does not support Sound mode. "
|
|
167
|
+
"Testing status.xml interface next"
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
if not self._is_setup:
|
|
171
|
+
self._support_sound_mode = True
|
|
172
|
+
_LOGGER.info("Sound mode supported")
|
|
163
173
|
return
|
|
164
|
-
|
|
174
|
+
|
|
175
|
+
urls = [self._device.urls.status, self._device.urls.mainzone]
|
|
176
|
+
# There are two different options of sound mode tags
|
|
177
|
+
try:
|
|
178
|
+
await self.async_update_attrs_status_xml(
|
|
179
|
+
self.status_xml_attrs_01, urls, cache_id=cache_id
|
|
180
|
+
)
|
|
181
|
+
except AvrProcessingError:
|
|
165
182
|
try:
|
|
166
183
|
await self.async_update_attrs_status_xml(
|
|
167
|
-
self.
|
|
184
|
+
self.status_xml_attrs_02, urls, cache_id=cache_id
|
|
168
185
|
)
|
|
169
186
|
except AvrProcessingError:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
_LOGGER.info("Sound mode not supported")
|
|
176
|
-
self._support_sound_mode = False
|
|
177
|
-
return
|
|
187
|
+
self._support_sound_mode = False
|
|
188
|
+
_LOGGER.info("Sound mode not supported")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if not self._is_setup:
|
|
178
192
|
self._support_sound_mode = True
|
|
193
|
+
_LOGGER.info("Sound mode supported")
|
|
179
194
|
|
|
180
195
|
def match_sound_mode(self) -> Optional[str]:
|
|
181
196
|
"""Match the raw_sound_mode to its corresponding sound_mode."""
|
|
@@ -248,7 +263,7 @@ class DenonAVRSoundMode(DenonAVRFoundation):
|
|
|
248
263
|
##############
|
|
249
264
|
@property
|
|
250
265
|
def support_sound_mode(self) -> Optional[bool]:
|
|
251
|
-
"""Return True if sound mode supported."""
|
|
266
|
+
"""Return True if sound mode is supported."""
|
|
252
267
|
return self._support_sound_mode
|
|
253
268
|
|
|
254
269
|
@property
|