denonavr 1.1.1__py3-none-any.whl → 1.2.0__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 CHANGED
@@ -17,7 +17,7 @@ from .ssdp import async_identify_denonavr_receivers
17
17
  logging.getLogger(__name__).addHandler(logging.NullHandler())
18
18
 
19
19
  __title__ = "denonavr"
20
- __version__ = "1.1.1"
20
+ __version__ = "1.2.0"
21
21
 
22
22
 
23
23
  async def async_discover():
denonavr/api.py CHANGED
@@ -16,18 +16,7 @@ import xml.etree.ElementTree as ET
16
16
  from collections import defaultdict
17
17
  from collections.abc import Hashable
18
18
  from io import BytesIO
19
- from typing import (
20
- Awaitable,
21
- Callable,
22
- Coroutine,
23
- DefaultDict,
24
- Dict,
25
- List,
26
- Optional,
27
- Set,
28
- Tuple,
29
- cast,
30
- )
19
+ from typing import Callable, DefaultDict, Dict, List, Optional, Set, Tuple, cast
31
20
 
32
21
  import attr
33
22
  import httpx
@@ -111,10 +100,11 @@ class HTTPXAsyncClient:
111
100
  """Call GET endpoint of Denon AVR receiver asynchronously."""
112
101
  client = self.client_getter()
113
102
  try:
114
- res = await client.get(
115
- url, timeout=httpx.Timeout(timeout, read=read_timeout)
116
- )
117
- res.raise_for_status()
103
+ async with client.stream(
104
+ "GET", url, timeout=httpx.Timeout(timeout, read=read_timeout)
105
+ ) as res:
106
+ res.raise_for_status()
107
+ await res.aread()
118
108
  finally:
119
109
  # Close the default AsyncClient but keep custom clients open
120
110
  if self.is_default_async_client():
@@ -137,13 +127,15 @@ class HTTPXAsyncClient:
137
127
  """Call GET endpoint of Denon AVR receiver asynchronously."""
138
128
  client = self.client_getter()
139
129
  try:
140
- res = await client.post(
130
+ async with client.stream(
131
+ "POST",
141
132
  url,
142
133
  content=content,
143
134
  data=data,
144
135
  timeout=httpx.Timeout(timeout, read=read_timeout),
145
- )
146
- res.raise_for_status()
136
+ ) as res:
137
+ res.raise_for_status()
138
+ await res.aread()
147
139
  finally:
148
140
  # Close the default AsyncClient but keep custom clients open
149
141
  if self.is_default_async_client():
@@ -475,6 +467,7 @@ class DenonAVRTelnetApi:
475
467
 
476
468
  host: str = attr.ib(converter=str, default="localhost")
477
469
  timeout: float = attr.ib(converter=float, default=2.0)
470
+ is_denon: bool = attr.ib(converter=bool, default=True)
478
471
  _connection_enabled: bool = attr.ib(default=False)
479
472
  _last_message_time: float = attr.ib(default=-1.0)
480
473
  _connect_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
@@ -484,7 +477,6 @@ class DenonAVRTelnetApi:
484
477
  _telnet_event_map: Dict[str, List] = attr.ib(
485
478
  default=attr.Factory(telnet_event_map_factory)
486
479
  )
487
- _callback_tasks: Set[asyncio.Task] = attr.ib(attr.Factory(set))
488
480
  _send_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
489
481
  _send_confirmation_timeout: float = attr.ib(converter=float, default=2.0)
490
482
  _send_confirmation_event: asyncio.Event = attr.ib(
@@ -492,20 +484,21 @@ class DenonAVRTelnetApi:
492
484
  )
493
485
  _send_confirmation_command: str = attr.ib(converter=str, default="")
494
486
  _send_tasks: Set[asyncio.Task] = attr.ib(attr.Factory(set))
495
- _callbacks: Dict[str, List[Coroutine]] = attr.ib(
487
+ _callbacks: Dict[str, List[Callable]] = attr.ib(
496
488
  validator=attr.validators.instance_of(dict),
497
489
  default=attr.Factory(dict),
498
490
  init=False,
499
491
  )
500
- _raw_callbacks: List[Coroutine] = attr.ib(
492
+ _raw_callbacks: List[Callable] = attr.ib(
501
493
  validator=attr.validators.instance_of(list),
502
494
  default=attr.Factory(list),
503
495
  init=False,
504
496
  )
497
+ _update_callback_tasks: Set[asyncio.Task] = attr.ib(attr.Factory(set))
505
498
 
506
499
  def __attrs_post_init__(self) -> None:
507
500
  """Initialize special attributes."""
508
- self._register_raw_callback(self._async_send_confirmation_callback)
501
+ self._register_raw_callback(self._send_confirmation_callback)
509
502
 
510
503
  async def async_connect(self) -> None:
511
504
  """Connect to the receiver asynchronously."""
@@ -549,70 +542,97 @@ class DenonAVRTelnetApi:
549
542
  self._connection_enabled = True
550
543
  self._last_message_time = time.monotonic()
551
544
  self._schedule_monitor()
552
- # Trigger update of all attributes
545
+
546
+ # Cancel all update tasks in case they are still running and create a new one.
547
+ for callback_task in self._update_callback_tasks:
548
+ callback_task.cancel()
549
+
550
+ task = asyncio.create_task(self._async_trigger_updates())
551
+ self._update_callback_tasks.add(task)
552
+ task.add_done_callback(self._update_callback_tasks.discard)
553
+
554
+ async def _async_trigger_updates(self) -> None:
555
+ """Trigger update of all attributes."""
556
+ commands = [
557
+ # Critical State Info
558
+ "ZM?", # Main Zone Power
559
+ "SI?", # Select INPUT source
560
+ "MV?", # MASTER VOLUME
561
+ "MU?", # Mute
562
+ "Z2?", # Z2 Zone Power
563
+ "Z2MU?", # Z2 Mute
564
+ "Z3?", # Z3 Zone Power
565
+ "Z3MU?", # Z3 Mute
566
+ "MS?", # Surround mode
567
+ # State Info used in Toggle Commands
568
+ "MNMEN?", # Menu
569
+ "TR?", # Trigger
570
+ "PSTONE CTRL ?", # TONE
571
+ "PSDYNEQ ?", # Dynamic EQ
572
+ "PSLFC ?", # Audyssey LFC
573
+ "PSNEURAL ?", # Neural:X
574
+ "PSIMAXAUD ?", # IMAX Audio Settings Auto/Manual
575
+ "PSIMAXSWM ?", # IMAX Subwoofer Mode
576
+ "PSSWR ?", # Subwoofer
577
+ "SSTTR ?", # Tactile Transducer
578
+ "VSAUDIO ?", # HDMI Audio Decode
579
+ "PSCES ?", # CENTER Spread
580
+ "PSLOM ?", # Loudness Management
581
+ "PSCINEMA EQ. ?", # CINEMA EQ
582
+ "BTTX ?", # Bluetooth Transmitter
583
+ "PSSPV ?", # Speaker Virtualizer
584
+ "PSGEQ ?", # Graphic EQ
585
+ "PSHEQ ?", # Headphone EQ
586
+ # Regular State Info
587
+ "PSBAS ?", # BASS
588
+ "PSTRE ?", # TREBLE
589
+ "PSCNTAMT ?", # Containment Amount
590
+ "PSMULTEQ: ?", # MultEQ
591
+ "PSREFLEV ?", # Reference Level
592
+ "PSDYNVOL ?", # Dynamic Vol.
593
+ "DIM ?", # Dimmer
594
+ "PSDELAY ?", # Audio Delay
595
+ "ECO?", # ECO
596
+ "VSMONI ?", # HDMI Output
597
+ "PSDIRAC ?", # Dirac Live Filter
598
+ "CV?", # Channel Volume
599
+ "PSIMAX ?", # IMAX
600
+ "PSIMAXHPF ?", # IMAX High Pass Filter
601
+ "PSIMAXLPF ?", # IMAX Low Pass Filter
602
+ "PSIMAXSWO ?", # Subwoofer Output LFE+Main/LFE
603
+ "PSSWL ?", # Subwoofer Level
604
+ "STBY?", # Auto Standby
605
+ "SLP?", # Sleep
606
+ "VSVPM ?", # Video Process
607
+ "PSLFE ?", # LFE Level
608
+ "PSBSC ?", # Bass Sync
609
+ "PSDEH ?", # Dialog Enhancer
610
+ "PSAUROPR ?", # Auro-Matic Preset
611
+ "PSAUROST ?", # Auro-Matic Strength
612
+ "PSAUROMODE ?", # AURO-3D Mode
613
+ "PSRSZ ?", # ROOM SIZE
614
+ "SPPR ?", # Speaker Preset
615
+ "PSDIC ?", # Dialog Control
616
+ "PSSP: ?", # Effect Speaker selection
617
+ "PSDRC ?", # DRC
618
+ "PSDEL ?", # DELAY TIME
619
+ "PSRSTR ?", # AUDIO RESTORER
620
+ ]
621
+
622
+ index = commands.index("MNMEN?")
623
+ if self.is_denon:
624
+ commands.insert(index := index + 1, "MSQUICK ?") # Quick Select
625
+ if not self.is_denon:
626
+ commands.insert(index + 1, "MSSMART ?") # SMART Select
627
+ index = commands.index("TR?")
628
+ commands.insert(index := index + 1, "PSMDAX ?") # MDAX
629
+ commands.insert(index := index + 1, "PSDACFIL ?") # DAC Filter
630
+ commands.insert(index := index + 1, "ILB ?") # Illumination
631
+ commands.insert(index + 1, "SSHOS ?") # Auto Lip Sync
632
+
553
633
  await self.async_send_commands(
554
- "ZM?",
555
- "SI?",
556
- "MV?",
557
- "MU?",
558
- "Z2?",
559
- "Z2MU?",
560
- "Z3?",
561
- "Z3MU?",
562
- "PSTONE CTRL ?",
563
- "PSBAS ?",
564
- "PSTRE ?",
565
- "PSDYNEQ ?",
566
- "PSLFC ?",
567
- "PSCNTAMT ?",
568
- "PSMULTEQ: ?",
569
- "PSREFLEV ?",
570
- "PSDYNVOL ?",
571
- "MS?",
572
- "MNMEN?",
573
- "DIM ?",
574
- "PSDELAY?",
575
- "ECO?",
576
- "VSMONI ?",
577
- "PSDIRAC ?",
578
- "CV?",
579
- "PSNEURAL ?",
580
- "PSIMAX ?",
581
- "PSIMAXAUD ?",
582
- "PSIMAXHPF ?",
583
- "PSIMAXLPF ?",
584
- "PSIMAXSWM ?",
585
- "PSIMAXSWO ?",
586
- "PSSWR ?",
587
- "PSSWL ?",
588
- "SSTTR ?",
589
- "MSQUICK ?",
590
- "STBY?",
591
- "SLP?",
592
- "VSAUDIO ?",
593
- "PSCES ?",
594
- "VSVPM ?",
595
- "PSLFE ?",
596
- "PSLOM ?",
597
- "PSBSC ?",
598
- "PSDEH ?",
599
- "PSCINEMA EQ. ?",
600
- "PSAUROPR ?",
601
- "PSAUROST ?",
602
- "PSAUROMODE ?",
603
- "PSRSZ ?",
604
- "TR?",
605
- "SPPR ?",
606
- "BTTX ?",
607
- "PSDIC ?",
608
- "PSSPV ?",
609
- "PSSP: ?",
610
- "PSDRC ?",
611
- "PSDEL ?",
612
- "PSRSTR ?",
613
- "PSGEQ ?",
614
- "PSHEQ ?",
615
- skip_confirmation=True,
634
+ *commands,
635
+ confirmation_timeout=0.2,
616
636
  )
617
637
 
618
638
  def _schedule_monitor(self) -> None:
@@ -712,7 +732,7 @@ class DenonAVRTelnetApi:
712
732
  self._reconnect_task = None
713
733
 
714
734
  def register_callback(
715
- self, event: str, callback: Callable[[str, str, str], Awaitable[None]]
735
+ self, event: str, callback: Callable[[str, str, str], None]
716
736
  ) -> None:
717
737
  """Register a callback handler for an event type."""
718
738
  # Validate the passed in type
@@ -726,24 +746,20 @@ class DenonAVRTelnetApi:
726
746
  self._callbacks[event].append(callback)
727
747
 
728
748
  def unregister_callback(
729
- self, event: str, callback: Callable[[str, str, str], Awaitable[None]]
749
+ self, event: str, callback: Callable[[str, str, str], None]
730
750
  ) -> None:
731
751
  """Unregister a callback handler for an event type."""
732
752
  if event not in self._callbacks.keys():
733
753
  return
734
754
  self._callbacks[event].remove(callback)
735
755
 
736
- def _register_raw_callback(
737
- self, callback: Callable[[str], Awaitable[None]]
738
- ) -> None:
756
+ def _register_raw_callback(self, callback: Callable[[str], None]) -> None:
739
757
  """Register a callback handler for raw telnet messages."""
740
758
  if callback in self._raw_callbacks:
741
759
  return
742
760
  self._raw_callbacks.append(callback)
743
761
 
744
- def _unregister_raw_callback(
745
- self, callback: Callable[[str], Awaitable[None]]
746
- ) -> None:
762
+ def _unregister_raw_callback(self, callback: Callable[[str], None]) -> None:
747
763
  """Unregister a callback handler for raw telnet messages."""
748
764
  self._raw_callbacks.remove(callback)
749
765
 
@@ -787,19 +803,15 @@ class DenonAVRTelnetApi:
787
803
  if event not in TELNET_EVENTS:
788
804
  return
789
805
 
790
- task = asyncio.create_task(
791
- self._async_run_callbacks(message, event, zone, parameter)
792
- )
793
- self._callback_tasks.add(task)
794
- task.add_done_callback(self._callback_tasks.discard)
806
+ self._run_callbacks(message, event, zone, parameter)
795
807
 
796
- async def _async_run_callbacks(
808
+ def _run_callbacks(
797
809
  self, message: str, event: str, zone: str, parameter: str
798
810
  ) -> None:
799
811
  """Handle triggering the registered callbacks."""
800
812
  for callback in self._raw_callbacks:
801
813
  try:
802
- await callback(message)
814
+ callback(message)
803
815
  except Exception as err: # pylint: disable=broad-except
804
816
  # We don't want a single bad callback to trip up the
805
817
  # whole system and prevent further execution
@@ -812,7 +824,7 @@ class DenonAVRTelnetApi:
812
824
  if event in self._callbacks.keys():
813
825
  for callback in self._callbacks[event]:
814
826
  try:
815
- await callback(zone, event, parameter)
827
+ callback(zone, event, parameter)
816
828
  except Exception as err: # pylint: disable=broad-except
817
829
  # We don't want a single bad callback to trip up the
818
830
  # whole system and prevent further execution
@@ -825,7 +837,7 @@ class DenonAVRTelnetApi:
825
837
  if ALL_TELNET_EVENTS in self._callbacks.keys():
826
838
  for callback in self._callbacks[ALL_TELNET_EVENTS]:
827
839
  try:
828
- await callback(zone, event, parameter)
840
+ callback(zone, event, parameter)
829
841
  except Exception as err: # pylint: disable=broad-except
830
842
  # We don't want a single bad callback to trip up the
831
843
  # whole system and prevent further execution
@@ -843,7 +855,7 @@ class DenonAVRTelnetApi:
843
855
  return event
844
856
  return ""
845
857
 
846
- async def _async_send_confirmation_callback(self, message: str) -> None:
858
+ def _send_confirmation_callback(self, message: str) -> None:
847
859
  """Confirm that the telnet command has been executed."""
848
860
  if len(message) < 3:
849
861
  return
@@ -854,9 +866,15 @@ class DenonAVRTelnetApi:
854
866
  _LOGGER.debug("Command %s confirmed", command)
855
867
 
856
868
  async def _async_send_command(
857
- self, command: str, skip_confirmation: bool = False
869
+ self,
870
+ command: str,
871
+ skip_confirmation: bool = False,
872
+ confirmation_timeout: Optional[float] = None,
858
873
  ) -> None:
859
874
  """Send one telnet command to the receiver."""
875
+ if confirmation_timeout is None:
876
+ confirmation_timeout = self._send_confirmation_timeout
877
+
860
878
  async with self._send_lock:
861
879
  if not skip_confirmation:
862
880
  self._send_confirmation_command = command
@@ -871,26 +889,42 @@ class DenonAVRTelnetApi:
871
889
  try:
872
890
  await asyncio.wait_for(
873
891
  self._send_confirmation_event.wait(),
874
- self._send_confirmation_timeout,
892
+ confirmation_timeout,
875
893
  )
876
894
  except asyncio.TimeoutError:
877
- _LOGGER.info(
895
+ _LOGGER.debug(
878
896
  "Timeout waiting for confirmation of command: %s", command
879
897
  )
880
898
  finally:
881
899
  self._send_confirmation_command = ""
882
900
 
883
901
  async def async_send_commands(
884
- self, *commands: str, skip_confirmation: bool = False
902
+ self,
903
+ *commands: str,
904
+ skip_confirmation: bool = False,
905
+ confirmation_timeout: Optional[float] = None,
885
906
  ) -> None:
886
907
  """Send telnet commands to the receiver."""
887
908
  for command in commands:
888
- await self._async_send_command(command, skip_confirmation=skip_confirmation)
909
+ await self._async_send_command(
910
+ command,
911
+ skip_confirmation=skip_confirmation,
912
+ confirmation_timeout=confirmation_timeout,
913
+ )
889
914
 
890
- def send_commands(self, *commands: str, skip_confirmation: bool = False) -> None:
915
+ def send_commands(
916
+ self,
917
+ *commands: str,
918
+ skip_confirmation: bool = False,
919
+ confirmation_timeout: Optional[float] = None,
920
+ ) -> None:
891
921
  """Send telnet commands to the receiver."""
892
922
  task = asyncio.create_task(
893
- self.async_send_commands(*commands, skip_confirmation=skip_confirmation)
923
+ self.async_send_commands(
924
+ *commands,
925
+ skip_confirmation=skip_confirmation,
926
+ confirmation_timeout=confirmation_timeout,
927
+ )
894
928
  )
895
929
  self._send_tasks.add(task)
896
930
  task.add_done_callback(self._send_tasks.discard)
denonavr/audyssey.py CHANGED
@@ -77,15 +77,11 @@ class DenonAVRAudyssey(DenonAVRFoundation):
77
77
  for tag in self.appcommand0300_attrs:
78
78
  self._device.api.add_appcommand0300_update_tag(tag)
79
79
 
80
- self._device.telnet_api.register_callback(
81
- "PS", self._async_sound_detail_callback
82
- )
80
+ self._device.telnet_api.register_callback("PS", self._sound_detail_callback)
83
81
 
84
82
  self._is_setup = True
85
83
 
86
- async def _async_sound_detail_callback(
87
- self, zone: str, event: str, parameter: str
88
- ) -> None:
84
+ def _sound_detail_callback(self, zone: str, event: str, parameter: str) -> None:
89
85
  """Handle a sound detail change event."""
90
86
  if self._device.zone != zone:
91
87
  return