denonavr 1.1.2__py3-none-any.whl → 1.3.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/input.py CHANGED
@@ -28,6 +28,7 @@ from .const import (
28
28
  DENON_ATTR_SETATTR,
29
29
  HDTUNER_SOURCES,
30
30
  MAIN_ZONE,
31
+ NETAUDIO_PLAYING,
31
32
  NETAUDIO_SOURCES,
32
33
  PLAYING_SOURCES,
33
34
  POWER_ON,
@@ -36,6 +37,7 @@ from .const import (
36
37
  STATE_ON,
37
38
  STATE_PAUSED,
38
39
  STATE_PLAYING,
40
+ STATE_STOPPED,
39
41
  STATIC_ALBUM_URL,
40
42
  TELNET_MAPPING,
41
43
  TUNER_SOURCES,
@@ -73,14 +75,16 @@ def set_input_func(
73
75
  if value is None:
74
76
  return value
75
77
  # Map from input_func_map
76
- # AirPlay and Internet Radio are not always listed in available sources
77
- if value in ["AirPlay", "Internet Radio"]:
78
- if value not in instance._input_func_map:
79
- instance._additional_input_funcs.add(value)
80
- instance._input_func_map[value] = value
81
- instance._input_func_map_rev[value] = value
82
- instance._netaudio_func_list.append(value)
83
- instance._playing_func_list.append(value)
78
+ # Some inputs are not always listed in available sources
79
+ if (
80
+ value in {"AirPlay", "Internet Radio", "Media Server", "NET"}
81
+ and value not in instance._input_func_map
82
+ ):
83
+ instance._additional_input_funcs.add(value)
84
+ instance._input_func_map[value] = value
85
+ instance._input_func_map_rev[value] = value
86
+ instance._netaudio_func_list.append(value)
87
+ instance._playing_func_list.append(value)
84
88
  try:
85
89
  input_func = instance._input_func_map_rev[value]
86
90
  except KeyError:
@@ -164,7 +168,7 @@ class DenonAVRInput(DenonAVRFoundation):
164
168
  _image_url: Optional[str] = attr.ib(
165
169
  converter=attr.converters.optional(str), default=None
166
170
  )
167
- _image_available: Optional[str] = attr.ib(
171
+ _image_available: Optional[bool] = attr.ib(
168
172
  converter=attr.converters.optional(str), default=None
169
173
  )
170
174
 
@@ -174,6 +178,8 @@ class DenonAVRInput(DenonAVRFoundation):
174
178
  ),
175
179
  default=attr.Factory(set),
176
180
  )
181
+ _callback_tasks: Set[asyncio.Task] = attr.ib(default=attr.Factory(set))
182
+ _netaudio_state: str = attr.ib(converter=fix_string, default="")
177
183
 
178
184
  # Update tags for attributes
179
185
  # AppCommand.xml interface
@@ -197,22 +203,18 @@ class DenonAVRInput(DenonAVRFoundation):
197
203
  power_event = "Z2"
198
204
  elif self._device.zone == ZONE3:
199
205
  power_event = "Z3"
206
+ self._device.telnet_api.register_callback(power_event, self._power_callback)
207
+ self._device.telnet_api.register_callback("SI", self._input_callback)
208
+ self._device.telnet_api.register_callback("NSE", self._netaudio_callback)
209
+ self._device.telnet_api.register_callback("TF", self._tuner_callback)
210
+ self._device.telnet_api.register_callback("HD", self._hdtuner_callback)
200
211
  self._device.telnet_api.register_callback(
201
- power_event, self._async_power_callback
202
- )
203
- self._device.telnet_api.register_callback("SI", self._async_input_callback)
204
- self._device.telnet_api.register_callback("NSE", self._async_netaudio_callback)
205
- self._device.telnet_api.register_callback("TF", self._async_tuner_callback)
206
- self._device.telnet_api.register_callback("HD", self._async_hdtuner_callback)
207
- self._device.telnet_api.register_callback(
208
- "SS", self._async_input_func_update_callback
212
+ "SS", self._input_func_update_callback
209
213
  )
210
214
 
211
215
  self._is_setup = True
212
216
 
213
- async def _async_input_callback(
214
- self, zone: str, event: str, parameter: str
215
- ) -> None:
217
+ def _input_callback(self, zone: str, event: str, parameter: str) -> None:
216
218
  """Handle an input change event."""
217
219
  if self._device.zone != zone:
218
220
  return
@@ -228,9 +230,7 @@ class DenonAVRInput(DenonAVRFoundation):
228
230
  self._unset_media_state()
229
231
  self._state = STATE_ON
230
232
 
231
- async def _async_power_callback(
232
- self, zone: str, event: str, parameter: str
233
- ) -> None:
233
+ def _power_callback(self, zone: str, event: str, parameter: str) -> None:
234
234
  """Handle a power change event."""
235
235
  if self._device.zone != zone:
236
236
  return
@@ -240,7 +240,7 @@ class DenonAVRInput(DenonAVRFoundation):
240
240
  self._unset_media_state()
241
241
  self._state = STATE_OFF
242
242
  elif self._schedule_media_updates():
243
- self._state = STATE_PLAYING
243
+ self._state = STATE_ON
244
244
  else:
245
245
  self._unset_media_state()
246
246
  self._state = STATE_ON
@@ -285,16 +285,24 @@ class DenonAVRInput(DenonAVRFoundation):
285
285
  else:
286
286
  self._stop_media_update()
287
287
 
288
- async def _async_netaudio_callback(
289
- self, zone: str, event: str, parameter: str
290
- ) -> None:
288
+ def _netaudio_callback(self, zone: str, event: str, parameter: str) -> None:
291
289
  """Handle a netaudio update event."""
292
290
  if self._device.power != POWER_ON:
293
291
  return
294
292
  if self._input_func not in self._netaudio_func_list:
295
293
  return
296
294
 
297
- if parameter.startswith("1"):
295
+ if parameter.startswith("0"):
296
+ if parameter[1:].startswith(NETAUDIO_PLAYING):
297
+ # It is not possible to see if the device is playing or paused.
298
+ # We assume it is playing first.
299
+ # Then, the state might be changed by async_play and async_pause.
300
+ if self._state not in {STATE_PLAYING, STATE_PAUSED}:
301
+ self._state = STATE_PLAYING
302
+ else:
303
+ self._state = STATE_STOPPED
304
+ self._netaudio_state = parameter[1:]
305
+ elif parameter.startswith("1"):
298
306
  self._title = parameter[1:]
299
307
  elif parameter.startswith("2"):
300
308
  self._artist = parameter[1:]
@@ -305,12 +313,13 @@ class DenonAVRInput(DenonAVRFoundation):
305
313
  self._station = None
306
314
 
307
315
  # Refresh cover with a hash for media URL when track is changing
308
- self._image_url = ALBUM_COVERS_URL.format(
309
- host=self._device.api.host,
310
- port=self._device.api.port,
311
- hash=hash((self._title, self._artist, self._album)),
316
+ self._set_image_url(
317
+ ALBUM_COVERS_URL.format(
318
+ host=self._device.api.host,
319
+ port=self._device.api.port,
320
+ hash=hash((self._title, self._artist, self._album)),
321
+ )
312
322
  )
313
- await self._async_test_image_accessible()
314
323
 
315
324
  def _schedule_tuner_update(self) -> None:
316
325
  """Schedule a tuner update task."""
@@ -330,9 +339,7 @@ class DenonAVRInput(DenonAVRFoundation):
330
339
  else:
331
340
  self._stop_media_update()
332
341
 
333
- async def _async_tuner_callback(
334
- self, zone: str, event: str, parameter: str
335
- ) -> None:
342
+ def _tuner_callback(self, zone: str, event: str, parameter: str) -> None:
336
343
  """Handle a tuner update event."""
337
344
  if self._device.power != POWER_ON:
338
345
  return
@@ -353,10 +360,11 @@ class DenonAVRInput(DenonAVRFoundation):
353
360
  self._album = None
354
361
 
355
362
  # No special cover, using a static one
356
- self._image_url = STATIC_ALBUM_URL.format(
357
- host=self._device.api.host, port=self._device.api.port
363
+ self._set_image_url(
364
+ STATIC_ALBUM_URL.format(
365
+ host=self._device.api.host, port=self._device.api.port
366
+ )
358
367
  )
359
- await self._async_test_image_accessible()
360
368
 
361
369
  def _schedule_hdtuner_update(self) -> None:
362
370
  """Schedule a HD tuner update task."""
@@ -374,9 +382,7 @@ class DenonAVRInput(DenonAVRFoundation):
374
382
  else:
375
383
  self._stop_media_update()
376
384
 
377
- async def _async_hdtuner_callback(
378
- self, zone: str, event: str, parameter: str
379
- ) -> None:
385
+ def _hdtuner_callback(self, zone: str, event: str, parameter: str) -> None:
380
386
  """Handle an HD tuner update event."""
381
387
  if self._device.power != POWER_ON:
382
388
  return
@@ -396,19 +402,35 @@ class DenonAVRInput(DenonAVRFoundation):
396
402
  self._frequency = None
397
403
 
398
404
  # No special cover, using a static one
399
- self._image_url = STATIC_ALBUM_URL.format(
400
- host=self._device.api.host, port=self._device.api.port
405
+ self._set_image_url(
406
+ STATIC_ALBUM_URL.format(
407
+ host=self._device.api.host, port=self._device.api.port
408
+ )
401
409
  )
402
- await self._async_test_image_accessible()
403
410
 
404
- async def _async_input_func_update_callback(
411
+ def _input_func_update_callback(
405
412
  self, zone: str, event: str, parameter: str
406
413
  ) -> None:
407
414
  """Handle input func update events."""
408
415
  if self._input_func_update_lock.locked():
409
416
  return
410
- async with self._input_func_update_lock:
411
- await self.async_update_inputfuncs()
417
+ task = asyncio.create_task(self.async_update_inputfuncs())
418
+ self._callback_tasks.add(task)
419
+ task.add_done_callback(self._callback_tasks.discard)
420
+
421
+ def _set_image_url(self, image_url: str) -> None:
422
+ """Set image URL if it is accessible."""
423
+ if self._image_available is False:
424
+ return
425
+
426
+ self._image_url = image_url
427
+
428
+ if self._image_available:
429
+ return
430
+
431
+ task = asyncio.create_task(self._async_test_image_accessible())
432
+ self._callback_tasks.add(task)
433
+ task.add_done_callback(self._callback_tasks.discard)
412
434
 
413
435
  async def async_update(
414
436
  self, global_update: bool = False, cache_id: Optional[Hashable] = None
@@ -525,7 +547,7 @@ class DenonAVRInput(DenonAVRFoundation):
525
547
 
526
548
  self._replace_duplicate_sources(renamed_sources)
527
549
 
528
- return (renamed_sources, deleted_sources)
550
+ return renamed_sources, deleted_sources
529
551
 
530
552
  async def async_get_changed_sources_status_xml(
531
553
  self, cache_id: Optional[Hashable] = None
@@ -615,22 +637,23 @@ class DenonAVRInput(DenonAVRFoundation):
615
637
 
616
638
  self._replace_duplicate_sources(renamed_sources)
617
639
 
618
- return (renamed_sources, deleted_sources)
640
+ return renamed_sources, deleted_sources
619
641
 
620
642
  async def async_update_inputfuncs(
621
643
  self, global_update: bool = False, cache_id: Optional[Hashable] = None
622
644
  ) -> None:
623
645
  """Update sources list from receiver."""
624
- if self._device.receiver in [AVR_X, AVR_X_2016]:
625
- await self._async_update_inputfuncs_avr_x(
626
- global_update=global_update, cache_id=cache_id
627
- )
628
- elif self._device.receiver in [AVR]:
629
- await self._async_update_inputfuncs_avr(cache_id=cache_id)
630
- else:
631
- raise AvrProcessingError(
632
- "Device is not setup correctly, receiver type not set"
633
- )
646
+ async with self._input_func_update_lock:
647
+ if self._device.receiver in [AVR_X, AVR_X_2016]:
648
+ await self._async_update_inputfuncs_avr_x(
649
+ global_update=global_update, cache_id=cache_id
650
+ )
651
+ elif self._device.receiver in [AVR]:
652
+ await self._async_update_inputfuncs_avr(cache_id=cache_id)
653
+ else:
654
+ raise AvrProcessingError(
655
+ "Device is not setup correctly, receiver type not set"
656
+ )
634
657
 
635
658
  async def _async_update_inputfuncs_avr_x(
636
659
  self, global_update: bool = False, cache_id: Optional[Hashable] = None
@@ -813,6 +836,7 @@ class DenonAVRInput(DenonAVRFoundation):
813
836
  "_title": "./szLine/value[2]",
814
837
  "_artist": "./szLine/value[3]",
815
838
  "_album": "./szLine/value[5]",
839
+ "_netaudio_state": "./szLine/value[1]",
816
840
  }
817
841
  self._band = None
818
842
  self._frequency = None
@@ -862,8 +886,15 @@ class DenonAVRInput(DenonAVRFoundation):
862
886
  port=self._device.api.port,
863
887
  hash=hash((self._title, self._artist, self._album)),
864
888
  )
865
- # On track change assume device is PLAYING
866
- self._state = STATE_PLAYING
889
+
890
+ if self._netaudio_state.startswith(NETAUDIO_PLAYING):
891
+ # It is not possible to see if the device is playing or paused.
892
+ # We assume it is playing first.
893
+ # Then, the state might be changed by async_play and async_pause.
894
+ if self._state not in {STATE_PLAYING, STATE_PAUSED}:
895
+ self._state = STATE_PLAYING
896
+ else:
897
+ self._state = STATE_STOPPED
867
898
 
868
899
  await self._async_test_image_accessible()
869
900
 
@@ -1070,13 +1101,9 @@ class DenonAVRInput(DenonAVRFoundation):
1070
1101
  await self.async_play()
1071
1102
 
1072
1103
  async def async_play(self) -> None:
1073
- """Send play command to receiver command via HTTP post."""
1104
+ """Send play command to receiver command."""
1074
1105
  # Use pause command only for sources which support NETAUDIO
1075
1106
  if self._input_func in self._netaudio_func_list:
1076
- # In fact play command is a play/pause toggle. Thus checking state
1077
- if self._state == STATE_PLAYING:
1078
- _LOGGER.info("Already playing, play command not sent")
1079
- return
1080
1107
  if self._device.telnet_available:
1081
1108
  await self._device.telnet_api.async_send_commands(
1082
1109
  self._device.telnet_commands.command_play
@@ -1086,12 +1113,12 @@ class DenonAVRInput(DenonAVRFoundation):
1086
1113
  self._state = STATE_PLAYING
1087
1114
 
1088
1115
  async def async_pause(self) -> None:
1089
- """Send pause command to receiver command via HTTP post."""
1116
+ """Send pause command to receiver command."""
1090
1117
  # Use pause command only for sources which support NETAUDIO
1091
1118
  if self._input_func in self._netaudio_func_list:
1092
1119
  if self._device.telnet_available:
1093
1120
  await self._device.telnet_api.async_send_commands(
1094
- self._device.telnet_commands.command_play
1121
+ self._device.telnet_commands.command_pause
1095
1122
  )
1096
1123
  else:
1097
1124
  await self._device.api.async_get_command(
@@ -1099,8 +1126,20 @@ class DenonAVRInput(DenonAVRFoundation):
1099
1126
  )
1100
1127
  self._state = STATE_PAUSED
1101
1128
 
1129
+ async def async_stop(self) -> None:
1130
+ """Send stop command to receiver command."""
1131
+ # Use stop command only for sources which support NETAUDIO
1132
+ if self._input_func in self._netaudio_func_list:
1133
+ if self._device.telnet_available:
1134
+ await self._device.telnet_api.async_send_commands(
1135
+ self._device.telnet_commands.command_stop
1136
+ )
1137
+ else:
1138
+ await self._device.api.async_get_command(self._device.urls.command_stop)
1139
+ self._state = STATE_STOPPED
1140
+
1102
1141
  async def async_previous_track(self) -> None:
1103
- """Send previous track command to receiver command via HTTP post."""
1142
+ """Send previous track command to receiver command."""
1104
1143
  # Use previous track button only for sources which support NETAUDIO
1105
1144
  if self._input_func in self._netaudio_func_list:
1106
1145
  body = {
@@ -1113,7 +1152,7 @@ class DenonAVRInput(DenonAVRFoundation):
1113
1152
  )
1114
1153
 
1115
1154
  async def async_next_track(self) -> None:
1116
- """Send next track command to receiver command via HTTP post."""
1155
+ """Send next track command to receiver command."""
1117
1156
  # Use next track button only for sources which support NETAUDIO
1118
1157
  if self._input_func in self._netaudio_func_list:
1119
1158
  body = {