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/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
- # Own setup
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
- timeout = self.api.timeout
150
- self.api.timeout = httpx.Timeout(self.api.timeout.connect)
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.timeout = timeout
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
- exc_info=err,
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 receivers"
218
+ "device is not a %s receiver: %s"
214
219
  ),
215
220
  r_type.port,
216
221
  r_type.type,
217
- exc_info=err,
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", exc_info=err
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", exc_info=err
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", exc_info=err)
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", exc_info=err)
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", exc_info=err)
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", exc_info=err)
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", exc_info=err)
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", exc_info=err)
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, exc_info=err
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", exc_info=err)
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
- exc_info=err,
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 and not ignore_missing_response:
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, exc_info=err
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
- exc_info=err,
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 and not ignore_missing_response:
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 = 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("TFAN?", "TFANNAME?")
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", exc_info=err)
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", exc_info=err)
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", exc_info=err)
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.async_client_getter()
873
+ client = self._device.api.httpx_async_client.client_getter()
856
874
  try:
857
875
  res = await client.get(
858
- self._image_url, timeout=self._device.api.timeout
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
- ALL_ZONE_STEREO,
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
- # Add tags for a potential AppCommand.xml update
109
- for tag in self.appcommand_attrs:
110
- self._device.api.add_appcommand_update_tag(tag)
111
-
112
- # Soundmode is always available for AVR-X and AVR-X-2016 receivers
113
- # For AVR receiver it will be tested during the first update
114
- if self._device.receiver in [AVR_X, AVR_X_2016]:
115
- self._support_sound_mode = True
116
- else:
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._device.use_avr_2016_update:
157
- await self.async_update_attrs_appcommand(
158
- self.appcommand_attrs, global_update=global_update, cache_id=cache_id
159
- )
160
- else:
161
- urls = [self._device.urls.status, self._device.urls.mainzone]
162
- if self._is_setup and not self._support_sound_mode:
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
- # There are two different options of sound mode tags
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.status_xml_attrs_01, urls, cache_id=cache_id
184
+ self.status_xml_attrs_02, urls, cache_id=cache_id
168
185
  )
169
186
  except AvrProcessingError:
170
- try:
171
- await self.async_update_attrs_status_xml(
172
- self.status_xml_attrs_02, urls, cache_id=cache_id
173
- )
174
- except AvrProcessingError:
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