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/foundation.py CHANGED
@@ -10,8 +10,9 @@ This module implements the foundation classes for Denon AVR receivers.
10
10
  import asyncio
11
11
  import logging
12
12
  import xml.etree.ElementTree as ET
13
+ from collections.abc import Hashable
13
14
  from copy import deepcopy
14
- from typing import Dict, Hashable, List, Optional
15
+ from typing import Dict, List, Optional, Union
15
16
 
16
17
  import attr
17
18
  import httpx
@@ -25,19 +26,24 @@ from .const import (
25
26
  AVR_X,
26
27
  AVR_X_2016,
27
28
  DENON_ATTR_SETATTR,
29
+ DENONAVR_TELNET_COMMANDS,
28
30
  DENONAVR_URLS,
29
31
  DESCRIPTION_TYPES,
30
32
  DEVICEINFO_AVR_X_PATTERN,
31
33
  DEVICEINFO_COMMAPI_PATTERN,
32
34
  MAIN_ZONE,
35
+ POWER_STATES,
33
36
  VALID_RECEIVER_TYPES,
34
37
  VALID_ZONES,
35
38
  ZONE2,
39
+ ZONE2_TELNET_COMMANDS,
36
40
  ZONE2_URLS,
37
41
  ZONE3,
42
+ ZONE3_TELNET_COMMANDS,
38
43
  ZONE3_URLS,
39
44
  ReceiverType,
40
45
  ReceiverURLs,
46
+ TelnetCommands,
41
47
  )
42
48
  from .exceptions import (
43
49
  AvrForbiddenError,
@@ -70,6 +76,9 @@ class DenonAVRDeviceInfo:
70
76
  validator=attr.validators.optional(attr.validators.in_(VALID_RECEIVER_TYPES)),
71
77
  default=None,
72
78
  )
79
+ telnet_commands: TelnetCommands = attr.ib(
80
+ validator=attr.validators.instance_of(TelnetCommands), init=False
81
+ )
73
82
  urls: ReceiverURLs = attr.ib(
74
83
  validator=attr.validators.instance_of(ReceiverURLs), init=False
75
84
  )
@@ -95,26 +104,29 @@ class DenonAVRDeviceInfo:
95
104
  converter=attr.converters.optional(str), default=None
96
105
  )
97
106
  _is_setup: bool = attr.ib(converter=bool, default=False, init=False)
98
- _allow_recovery: bool = attr.ib(converter=bool, default=False, init=False)
107
+ _allow_recovery: bool = attr.ib(converter=bool, default=True, init=True)
99
108
  _setup_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock))
100
109
 
101
110
  def __attrs_post_init__(self) -> None:
102
111
  """Initialize special attributes and callbacks."""
103
112
  # URLs depending from value of self.zone attribute
104
113
  if self.zone == MAIN_ZONE:
114
+ self.telnet_commands = DENONAVR_TELNET_COMMANDS
105
115
  self.urls = DENONAVR_URLS
106
116
  elif self.zone == ZONE2:
117
+ self.telnet_commands = ZONE2_TELNET_COMMANDS
107
118
  self.urls = ZONE2_URLS
108
119
  elif self.zone == ZONE3:
120
+ self.telnet_commands = ZONE3_TELNET_COMMANDS
109
121
  self.urls = ZONE3_URLS
110
122
  else:
111
- raise ValueError("Invalid zone {}".format(self.zone))
123
+ raise ValueError(f"Invalid zone {self.zone}")
112
124
 
113
125
  async def _async_power_callback(
114
126
  self, zone: str, event: str, parameter: str
115
127
  ) -> None:
116
128
  """Handle a power change event."""
117
- if self.zone == zone:
129
+ if self.zone == zone and parameter in POWER_STATES:
118
130
  self._power = parameter
119
131
 
120
132
  def get_own_zone(self) -> str:
@@ -146,7 +158,12 @@ class DenonAVRDeviceInfo:
146
158
  # Add tags for a potential AppCommand.xml update
147
159
  self.api.add_appcommand_update_tag(AppCommands.GetAllZonePowerStatus)
148
160
 
149
- self.telnet_api.register_callback("PW", self._async_power_callback)
161
+ power_event = "ZM"
162
+ if self.zone == ZONE2:
163
+ power_event = "Z2"
164
+ elif self.zone == ZONE3:
165
+ power_event = "Z3"
166
+ self.telnet_api.register_callback(power_event, self._async_power_callback)
150
167
 
151
168
  self._is_setup = True
152
169
 
@@ -155,7 +172,7 @@ class DenonAVRDeviceInfo:
155
172
  ) -> None:
156
173
  """Update status asynchronously."""
157
174
  # Ensure instance is setup before updating
158
- if self._is_setup is False:
175
+ if not self._is_setup:
159
176
  await self.async_setup()
160
177
 
161
178
  # Update power status
@@ -163,7 +180,7 @@ class DenonAVRDeviceInfo:
163
180
 
164
181
  async def async_identify_receiver(self) -> None:
165
182
  """Identify receiver asynchronously."""
166
- # Test Deviceinfo.xml if receiver is a AVR-X with port 80 for pre 2016
183
+ # Test Deviceinfo.xml if receiver is an AVR-X with port 80 for pre 2016
167
184
  # devices and port 8080 devices 2016 and later
168
185
  # 2016 models has also some of the XML but not all, try first 2016
169
186
  r_types = [AVR_X, AVR_X_2016]
@@ -184,15 +201,17 @@ class DenonAVRDeviceInfo:
184
201
  exc_info=err,
185
202
  )
186
203
 
187
- # Raise error only when occured at both types
204
+ # Raise error only when occurred at both types
188
205
  timeout_errors += 1
189
206
  if timeout_errors == len(r_types):
190
207
  raise
191
208
 
192
209
  except AvrRequestError as err:
193
210
  _LOGGER.debug(
194
- "Request error on port %s when identifying receiver, "
195
- "device is not a %s receivers",
211
+ (
212
+ "Request error on port %s when identifying receiver, "
213
+ "device is not a %s receivers"
214
+ ),
196
215
  r_type.port,
197
216
  r_type.type,
198
217
  exc_info=err,
@@ -204,13 +223,13 @@ class DenonAVRDeviceInfo:
204
223
  # Receiver identified, return
205
224
  return
206
225
 
207
- # If check of Deviceinfo.xml was not successfull, receiver is type AVR
226
+ # If check of Deviceinfo.xml was not successful, receiver is type AVR
208
227
  self.receiver = AVR
209
228
  self.api.port = AVR.port
210
229
 
211
230
  @staticmethod
212
231
  def _is_avr_x(deviceinfo: ET.Element) -> bool:
213
- """Evaluate Deviceinfo.xml if the device is a AVR-X device."""
232
+ """Evaluate Deviceinfo.xml if the device is an AVR-X device."""
214
233
  # First test by CommApiVers
215
234
  try:
216
235
  if bool(
@@ -270,7 +289,7 @@ class DenonAVRDeviceInfo:
270
289
  _LOGGER.info("AVR-X device, using AppCommand.xml interface")
271
290
  self._set_friendly_name(xml)
272
291
 
273
- if self.use_avr_2016_update is False:
292
+ if not self.use_avr_2016_update:
274
293
  try:
275
294
  xml = await self.api.async_get_xml(self.urls.mainzone)
276
295
  except (AvrTimoutError, AvrNetworkError) as err:
@@ -294,7 +313,7 @@ class DenonAVRDeviceInfo:
294
313
  ) -> None:
295
314
  """Verify if avr 2016 update method is working."""
296
315
  # Nothing to do if Appcommand.xml interface is not supported
297
- if self.use_avr_2016_update is False:
316
+ if self._is_setup and not self.use_avr_2016_update:
298
317
  return
299
318
 
300
319
  try:
@@ -306,7 +325,7 @@ class DenonAVRDeviceInfo:
306
325
  except AvrForbiddenError:
307
326
  # Recovery in case receiver changes port from 80 to 8080 which
308
327
  # might happen at Denon AVR-X 2016 receivers
309
- if self._allow_recovery is True:
328
+ if self._allow_recovery:
310
329
  self._allow_recovery = False
311
330
  _LOGGER.warning(
312
331
  "AppCommand.xml returns HTTP status 403. Running setup"
@@ -328,7 +347,7 @@ class DenonAVRDeviceInfo:
328
347
  )
329
348
  self.use_avr_2016_update = False
330
349
  else:
331
- if self._allow_recovery is False:
350
+ if not self._allow_recovery:
332
351
  _LOGGER.info("AppCommand.xml recovered from HTTP status 403 error")
333
352
  self._allow_recovery = True
334
353
 
@@ -353,9 +372,7 @@ class DenonAVRDeviceInfo:
353
372
  """Get device information."""
354
373
  port = DESCRIPTION_TYPES[self.receiver.type].port
355
374
  command = DESCRIPTION_TYPES[self.receiver.type].url
356
- url = "http://{host}:{port}{command}".format(
357
- host=self.api.host, port=port, command=command
358
- )
375
+ url = f"http://{self.api.host}:{port}{command}"
359
376
 
360
377
  device_info = None
361
378
  try:
@@ -368,8 +385,10 @@ class DenonAVRDeviceInfo:
368
385
  raise
369
386
  except AvrRequestError as err:
370
387
  _LOGGER.error(
371
- "During DenonAVR device identification, when trying to request"
372
- " %s the following error occurred: %s",
388
+ (
389
+ "During DenonAVR device identification, when trying to request"
390
+ " %s the following error occurred: %s"
391
+ ),
373
392
  url,
374
393
  err,
375
394
  )
@@ -381,9 +400,11 @@ class DenonAVRDeviceInfo:
381
400
  self.model_name = "Unknown"
382
401
  self.serial_number = None
383
402
  _LOGGER.warning(
384
- "Unable to get device information of host %s, Device might be "
385
- "in a corrupted state. Continuing without device information. "
386
- "Disconnect and reconnect power to the device and try again.",
403
+ (
404
+ "Unable to get device information of host %s, Device might be "
405
+ "in a corrupted state. Continuing without device information. "
406
+ "Disconnect and reconnect power to the device and try again."
407
+ ),
387
408
  self.api.host,
388
409
  )
389
410
  return
@@ -398,16 +419,17 @@ class DenonAVRDeviceInfo:
398
419
  self, global_update: bool = False, cache_id: Optional[Hashable] = None
399
420
  ) -> None:
400
421
  """Update power status of device."""
401
- if self.use_avr_2016_update is True:
422
+ if self.use_avr_2016_update is None:
423
+ raise AvrProcessingError(
424
+ "Device is not setup correctly, update method not set"
425
+ )
426
+
427
+ if self.use_avr_2016_update:
402
428
  await self.async_update_power_appcommand(
403
429
  global_update=global_update, cache_id=cache_id
404
430
  )
405
- elif self.use_avr_2016_update is False:
406
- await self.async_update_power_status_xml(cache_id=cache_id)
407
431
  else:
408
- raise AvrProcessingError(
409
- "Device is not setup correctly, update method not set"
410
- )
432
+ await self.async_update_power_status_xml(cache_id=cache_id)
411
433
 
412
434
  async def async_update_power_appcommand(
413
435
  self, global_update: bool = False, cache_id: Optional[Hashable] = None
@@ -430,14 +452,12 @@ class DenonAVRDeviceInfo:
430
452
 
431
453
  # Search for power tag
432
454
  power_tag = xml.find(
433
- "./cmd[@{attribute}='{cmd}']/{zone}".format(
434
- attribute=APPCOMMAND_CMD_TEXT, cmd=power_appcommand.cmd_text, zone=zone
435
- )
455
+ f"./cmd[@{APPCOMMAND_CMD_TEXT}='{power_appcommand.cmd_text}']/{zone}"
436
456
  )
437
457
 
438
458
  if power_tag is None:
439
459
  raise AvrProcessingError(
440
- "Power attribute of zone {} not found on update".format(self.zone)
460
+ f"Power attribute of zone {self.zone} not found on update"
441
461
  )
442
462
 
443
463
  self._power = power_tag.text
@@ -451,7 +471,7 @@ class DenonAVRDeviceInfo:
451
471
  if self.zone == MAIN_ZONE:
452
472
  urls.append(self.urls.mainzone)
453
473
  else:
454
- urls.append("{}?ZoneName={}".format(self.urls.mainzone, self.zone))
474
+ urls.append(f"{self.urls.mainzone}?ZoneName={self.zone}")
455
475
  # Tags in XML which might contain information about zones power status
456
476
  # ordered by their priority
457
477
  tags = ["./ZonePower/value", "./Power/value"]
@@ -473,7 +493,7 @@ class DenonAVRDeviceInfo:
473
493
  return
474
494
 
475
495
  raise AvrProcessingError(
476
- "Power attribute of zone {} not found on update".format(self.zone)
496
+ f"Power attribute of zone {self.zone} not found on update"
477
497
  )
478
498
 
479
499
  ##############
@@ -488,17 +508,32 @@ class DenonAVRDeviceInfo:
488
508
  """
489
509
  return self._power
490
510
 
511
+ @property
512
+ def telnet_available(self) -> bool:
513
+ """Return true if telnet is connected and healthy."""
514
+ return self.telnet_api.connected and self.telnet_api.healthy
515
+
491
516
  ##########
492
517
  # Setter #
493
518
  ##########
494
519
 
495
520
  async def async_power_on(self) -> None:
496
521
  """Turn on receiver via HTTP get command."""
497
- await self.api.async_get_command(self.urls.command_power_on)
522
+ if self.telnet_available:
523
+ await self.telnet_api.async_send_commands(
524
+ self.telnet_commands.command_power_on
525
+ )
526
+ else:
527
+ await self.api.async_get_command(self.urls.command_power_on)
498
528
 
499
529
  async def async_power_off(self) -> None:
500
530
  """Turn off receiver via HTTP get command."""
501
- await self.api.async_get_command(self.urls.command_power_standby)
531
+ if self.telnet_available:
532
+ await self.telnet_api.async_send_commands(
533
+ self.telnet_commands.command_power_standby
534
+ )
535
+ else:
536
+ await self.api.async_get_command(self.urls.command_power_standby)
502
537
 
503
538
 
504
539
  @attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR)
@@ -592,13 +627,12 @@ class DenonAVRFoundation:
592
627
  update_attrs.pop(app_command, None)
593
628
 
594
629
  # Check if each attribute was updated
595
- if update_attrs and ignore_missing_response is False:
630
+ if update_attrs and not ignore_missing_response:
596
631
  raise AvrProcessingError(
597
- "Some attributes of zone {} not found on update: {}".format(
598
- self._device.zone, update_attrs
599
- )
632
+ f"Some attributes of zone {self._device.zone} not found on update:"
633
+ f" {update_attrs}"
600
634
  )
601
- if update_attrs and ignore_missing_response is True:
635
+ if update_attrs and ignore_missing_response:
602
636
  _LOGGER.debug(
603
637
  "Some attributes of zone %s not found on update: %s",
604
638
  self._device.zone,
@@ -660,11 +694,10 @@ class DenonAVRFoundation:
660
694
  break
661
695
 
662
696
  # Check if each attribute was updated
663
- if update_attrs and ignore_missing_response is False:
697
+ if update_attrs and not ignore_missing_response:
664
698
  raise AvrProcessingError(
665
- "Some attributes of zone {} not found on update: {}".format(
666
- self._device.zone, update_attrs
667
- )
699
+ f"Some attributes of zone {self._device.zone} not found on update:"
700
+ f" {update_attrs}"
668
701
  )
669
702
 
670
703
  @staticmethod
@@ -678,17 +711,15 @@ class DenonAVRFoundation:
678
711
  string = "./cmd"
679
712
  # Text of cmd tag in query was added as attribute to response
680
713
  if app_command_cmd.cmd_text:
681
- string = string + "[@{}='{}']".format(
682
- APPCOMMAND_CMD_TEXT, app_command_cmd.cmd_text
714
+ string = (
715
+ string + f"[@{APPCOMMAND_CMD_TEXT}='{app_command_cmd.cmd_text}']"
683
716
  )
684
717
  # Text of name tag in query was added as attribute to response
685
718
  if app_command_cmd.name:
686
- string = string + "[@{}='{}']".format(
687
- APPCOMMAND_NAME, app_command_cmd.name
688
- )
719
+ string = string + f"[@{APPCOMMAND_NAME}='{app_command_cmd.name}']"
689
720
  # Some results include a zone tag
690
721
  if resp.add_zone:
691
- string = string + "/{}".format(zone)
722
+ string = string + f"/{zone}"
692
723
  # Suffix like /status, /volume
693
724
  string = string + resp.suffix
694
725
 
@@ -722,8 +753,10 @@ def set_api_timeout(
722
753
  return value
723
754
 
724
755
 
725
- def convert_string_int_bool(value: str) -> bool:
756
+ def convert_string_int_bool(value: Union[str, bool]) -> bool:
726
757
  """Convert an integer from string format to bool."""
727
758
  if value is None:
728
759
  return None
760
+ if isinstance(value, bool):
761
+ return value
729
762
  return bool(int(value))