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/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, Hashable, List, Optional, Tuple
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 unescape_string(value: Optional[str]) -> Optional[str]:
60
- """Perform HTML unescape on value."""
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 html.unescape(str(value))
64
+ return fix_text(str(value)).strip()
64
65
 
65
66
 
66
67
  def set_input_func(
67
- instance: DenonAVRFoundation, attribute: attr.Attribute, value: str
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(unescape_string), default=None
147
+ converter=attr.converters.optional(fix_string), default=None
138
148
  )
139
149
  _album: Optional[str] = attr.ib(
140
- converter=attr.converters.optional(unescape_string), default=None
150
+ converter=attr.converters.optional(fix_string), default=None
141
151
  )
142
152
  _band: Optional[str] = attr.ib(
143
- converter=attr.converters.optional(unescape_string), default=None
153
+ converter=attr.converters.optional(fix_string), default=None
144
154
  )
145
155
  _title: Optional[str] = attr.ib(
146
- converter=attr.converters.optional(unescape_string), default=None
156
+ converter=attr.converters.optional(fix_string), default=None
147
157
  )
148
158
  _frequency: Optional[str] = attr.ib(
149
- converter=attr.converters.optional(unescape_string), default=None
159
+ converter=attr.converters.optional(fix_string), default=None
150
160
  )
151
161
  _station: Optional[str] = attr.ib(
152
- converter=attr.converters.optional(unescape_string), default=None
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("PW", self._async_power_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() is True:
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() is True:
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.telnet_api.send_commands("NSE")
259
- self._schedule_netaudio_update()
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("E1"):
271
- self._title = parameter[2:].strip()
272
- elif parameter.startswith("E2"):
273
- self._artist = parameter[2:].strip()
274
- elif parameter.startswith("E4"):
275
- self._album = parameter[2:].strip()
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._async_test_image_accessable()
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.telnet_api.send_commands("TFAN?", "TFANNAME?")
299
- self._schedule_tuner_update()
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:].strip()
334
+ self._station = parameter[6:]
312
335
  elif len(parameter) == 8:
313
- self._frequency = "{}.{}".format(parameter[2:6], parameter[6:]).strip("0")
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._async_test_image_accessable()
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.telnet_api.send_commands("HD?")
340
- self._schedule_hdtuner_update()
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:].strip()
378
+ self._artist = parameter[6:]
353
379
  elif parameter.startswith("TITLE"):
354
- self._title = parameter[5:].strip()
380
+ self._title = parameter[5:]
355
381
  elif parameter.startswith("ALBUM"):
356
- self._album = parameter[5:].strip()
382
+ self._album = parameter[5:]
357
383
  elif parameter.startswith("ST NAME"):
358
- self._station = parameter[7:].strip()
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._async_test_image_accessable()
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() is True:
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 is False:
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("FuncName").text
430
- ] = xml_source.find("DefaultName").text
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[@{attribute}='{cmd}']/functionrename/list".format(
466
- attribute=APPCOMMAND_CMD_TEXT, cmd=AppCommands.GetRenameSource.cmd_text
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[@{attribute}='{cmd}']/functiondelete/list".format(
478
- attribute=APPCOMMAND_CMD_TEXT, cmd=AppCommands.GetDeletedSource.cmd_text
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 {}".format(
523
- self._device.receiver.type
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 is True:
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 is False:
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 inconsistant
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 is False:
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 True:
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
- elif self._device.use_avr_2016_update is False:
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._async_test_image_accessable()
850
+ await self._async_test_image_accessible()
819
851
 
820
- async def _async_test_image_accessable(self) -> None:
821
- """Test if image URL is accessable."""
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 is False:
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 is True:
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 {}".format(input_func)
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
- await self._device.api.async_get_command(command_url)
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
- await self._device.api.async_get_command(self._device.urls.command_play)
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
- await self._device.api.async_get_command(self._device.urls.command_pause)
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, Hashable, List, Optional
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: DenonAVRFoundation) -> Dict[str, str]:
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 druing the first update
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 is False:
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 True:
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
- elif self._device.use_avr_2016_update is False:
160
+ else:
155
161
  urls = [self._device.urls.status, self._device.urls.mainzone]
156
- if self._support_sound_mode is False:
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
- await self._device.api.async_get_command(command_url)
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) -> None:
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
- await self._device.api.async_get_command(command_url)
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 = "{xmlns}device".format(xmlns=SCPD_XMLNS)
39
- SCPD_DEVICELIST = "{xmlns}deviceList".format(xmlns=SCPD_XMLNS)
40
- SCPD_DEVICETYPE = "{xmlns}deviceType".format(xmlns=SCPD_XMLNS)
41
- SCPD_MANUFACTURER = "{xmlns}manufacturer".format(xmlns=SCPD_XMLNS)
42
- SCPD_MODELNAME = "{xmlns}modelName".format(xmlns=SCPD_XMLNS)
43
- SCPD_SERIALNUMBER = "{xmlns}serialNumber".format(xmlns=SCPD_XMLNS)
44
- SCPD_FRIENDLYNAME = "{xmlns}friendlyName".format(xmlns=SCPD_XMLNS)
45
- SCPD_PRESENTATIONURL = "{xmlns}presentationURL".format(xmlns=SCPD_XMLNS)
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: {}".format(ssdp_st),
61
- "MX: {:d}".format(ssdp_mx),
60
+ f"ST: {ssdp_st}",
61
+ f"MX: {ssdp_mx:d}",
62
62
  'MAN: "ssdp:discover"',
63
- "HOST: {}:{}".format(*SSDP_TARGET),
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 adresses
137
- if re.search("169.254.*.*", ip_addr):
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