denonavr 1.0.0__tar.gz → 1.0.1__tar.gz
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-1.0.0 → denonavr-1.0.1}/PKG-INFO +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/__init__.py +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/api.py +136 -60
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/decorators.py +5 -32
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/denonavr.py +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/foundation.py +6 -7
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/input.py +44 -3
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/PKG-INFO +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/pyproject.toml +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/test_denonavr.py +12 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/LICENSE +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/README.md +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/appcommand.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/audyssey.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/const.py +1 -1
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/exceptions.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/soundmode.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/ssdp.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/tonecontrol.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr/volume.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/SOURCES.txt +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/dependency_links.txt +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/not-zip-safe +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/requires.txt +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/denonavr.egg-info/top_level.txt +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/setup.cfg +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/__init__.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/test_ssdp.py +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AV7703-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AV7703-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AV7703-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AV7703-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AV7703-Deviceinfo-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-8500H-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-8500H-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-8500H-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-8500H-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-8500H-Deviceinfo-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-X3700H-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-X3700H-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-X3700H-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-X3700H-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVC-X3700H-Deviceinfo-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1713-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-1912-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-2312CI-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3311CI-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3312-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-3313-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-4810-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1100W-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X1600H_upnp.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-2-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2000-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-2-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X2100W-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4000-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4100W-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4300H-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4300H-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4300H-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4300H-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/AVR-X4300H-Deviceinfo-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-2-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR510-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-CR603-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/M-RC610-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1604-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1609-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1609-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1609-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1609-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/NR1609-Deviceinfo-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR5008-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-AppCommand-setup.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-AppCommand-update-soundmode.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-AppCommand-update-tonecontrol.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-AppCommand-update.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-Deviceinfo.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formMainZone_MainZoneXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formMainZone_MainZoneXmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formNetAudio_StatusXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formTuner_HdXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formTuner_TunerXml.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formZone2_Zone2XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6011-formZone3_Zone3XmlStatus.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6012-AppCommand-setup-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6012-AppCommand-update-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6012-AppCommand-update-soundmode-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6012-AppCommand-update-tonecontrol-8080.xml +0 -0
- {denonavr-1.0.0 → denonavr-1.0.1}/tests/xml/SR6012-Deviceinfo-8080.xml +0 -0
|
@@ -31,7 +31,8 @@ from typing import (
|
|
|
31
31
|
|
|
32
32
|
import attr
|
|
33
33
|
import httpx
|
|
34
|
-
from defusedxml
|
|
34
|
+
from defusedxml import DefusedXmlException
|
|
35
|
+
from defusedxml.ElementTree import ParseError, fromstring
|
|
35
36
|
|
|
36
37
|
from .appcommand import AppCommandCmd
|
|
37
38
|
from .const import (
|
|
@@ -83,16 +84,86 @@ def telnet_event_map_factory() -> Dict[str, List]:
|
|
|
83
84
|
return dict(event_map)
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
@attr.s(auto_attribs=True, hash=False
|
|
87
|
+
@attr.s(auto_attribs=True, hash=False)
|
|
88
|
+
class HTTPXAsyncClient:
|
|
89
|
+
"""Perform cached HTTP calls with httpx.AsyncClient."""
|
|
90
|
+
|
|
91
|
+
client_getter: Callable[[], httpx.AsyncClient] = attr.ib(
|
|
92
|
+
validator=attr.validators.is_callable(),
|
|
93
|
+
default=get_default_async_client,
|
|
94
|
+
init=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __hash__(self) -> int:
|
|
98
|
+
"""Hash the class using its ID that caching works."""
|
|
99
|
+
return id(self)
|
|
100
|
+
|
|
101
|
+
@cache_result
|
|
102
|
+
@async_handle_receiver_exceptions
|
|
103
|
+
async def async_get(
|
|
104
|
+
self,
|
|
105
|
+
url: str,
|
|
106
|
+
timeout: float,
|
|
107
|
+
read_timeout: float,
|
|
108
|
+
*,
|
|
109
|
+
cache_id: Hashable = None,
|
|
110
|
+
) -> httpx.Response:
|
|
111
|
+
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
112
|
+
client = self.client_getter()
|
|
113
|
+
try:
|
|
114
|
+
res = await client.get(
|
|
115
|
+
url, timeout=httpx.Timeout(timeout, read=read_timeout)
|
|
116
|
+
)
|
|
117
|
+
res.raise_for_status()
|
|
118
|
+
finally:
|
|
119
|
+
# Close the default AsyncClient but keep custom clients open
|
|
120
|
+
if self.is_default_async_client():
|
|
121
|
+
await client.aclose()
|
|
122
|
+
|
|
123
|
+
return res
|
|
124
|
+
|
|
125
|
+
@cache_result
|
|
126
|
+
@async_handle_receiver_exceptions
|
|
127
|
+
async def async_post(
|
|
128
|
+
self,
|
|
129
|
+
url: str,
|
|
130
|
+
timeout: float,
|
|
131
|
+
read_timeout: float,
|
|
132
|
+
*,
|
|
133
|
+
content: Optional[bytes] = None,
|
|
134
|
+
data: Optional[Dict] = None,
|
|
135
|
+
cache_id: Hashable = None,
|
|
136
|
+
) -> httpx.Response:
|
|
137
|
+
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
138
|
+
client = self.client_getter()
|
|
139
|
+
try:
|
|
140
|
+
res = await client.post(
|
|
141
|
+
url,
|
|
142
|
+
content=content,
|
|
143
|
+
data=data,
|
|
144
|
+
timeout=httpx.Timeout(timeout, read=read_timeout),
|
|
145
|
+
)
|
|
146
|
+
res.raise_for_status()
|
|
147
|
+
finally:
|
|
148
|
+
# Close the default AsyncClient but keep custom clients open
|
|
149
|
+
if self.is_default_async_client():
|
|
150
|
+
await client.aclose()
|
|
151
|
+
|
|
152
|
+
return res
|
|
153
|
+
|
|
154
|
+
def is_default_async_client(self) -> bool:
|
|
155
|
+
"""Check if default httpx.AsyncClient getter is used."""
|
|
156
|
+
return self.client_getter is get_default_async_client
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR)
|
|
87
160
|
class DenonAVRApi:
|
|
88
161
|
"""Perform API calls to Denon AVR REST interface."""
|
|
89
162
|
|
|
90
163
|
host: str = attr.ib(converter=str, default="localhost")
|
|
91
164
|
port: int = attr.ib(converter=int, default=80)
|
|
92
|
-
timeout:
|
|
93
|
-
|
|
94
|
-
default=httpx.Timeout(2.0, read=15.0),
|
|
95
|
-
)
|
|
165
|
+
timeout: float = attr.ib(converter=float, default=2.0)
|
|
166
|
+
read_timeout: float = attr.ib(converter=float, default=15.0)
|
|
96
167
|
_appcommand_update_tags: Tuple[AppCommandCmd] = attr.ib(
|
|
97
168
|
validator=attr.validators.deep_iterable(
|
|
98
169
|
attr.validators.instance_of(AppCommandCmd),
|
|
@@ -107,23 +178,18 @@ class DenonAVRApi:
|
|
|
107
178
|
),
|
|
108
179
|
default=attr.Factory(tuple),
|
|
109
180
|
)
|
|
110
|
-
|
|
111
|
-
validator=attr.validators.
|
|
112
|
-
default=
|
|
181
|
+
httpx_async_client: HTTPXAsyncClient = attr.ib(
|
|
182
|
+
validator=attr.validators.instance_of(HTTPXAsyncClient),
|
|
183
|
+
default=attr.Factory(HTTPXAsyncClient),
|
|
113
184
|
init=False,
|
|
114
185
|
)
|
|
115
186
|
|
|
116
|
-
def __hash__(self) -> int:
|
|
117
|
-
"""
|
|
118
|
-
Hash the class in a custom way that caching works.
|
|
119
|
-
|
|
120
|
-
It should react on changes of host and port.
|
|
121
|
-
"""
|
|
122
|
-
return hash((self.host, self.port))
|
|
123
|
-
|
|
124
|
-
@async_handle_receiver_exceptions
|
|
125
187
|
async def async_get(
|
|
126
|
-
self,
|
|
188
|
+
self,
|
|
189
|
+
request: str,
|
|
190
|
+
*,
|
|
191
|
+
port: Optional[int] = None,
|
|
192
|
+
cache_id: Hashable = None,
|
|
127
193
|
) -> httpx.Response:
|
|
128
194
|
"""Call GET endpoint of Denon AVR receiver asynchronously."""
|
|
129
195
|
# Use default port of the receiver if no different port is specified
|
|
@@ -131,24 +197,18 @@ class DenonAVRApi:
|
|
|
131
197
|
|
|
132
198
|
endpoint = f"http://{self.host}:{port}{request}"
|
|
133
199
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
res.raise_for_status()
|
|
138
|
-
finally:
|
|
139
|
-
# Close the default AsyncClient but keep custom clients open
|
|
140
|
-
if self.is_default_async_client():
|
|
141
|
-
await client.aclose()
|
|
142
|
-
|
|
143
|
-
return res
|
|
200
|
+
return await self.httpx_async_client.async_get(
|
|
201
|
+
endpoint, self.timeout, self.read_timeout, cache_id=cache_id
|
|
202
|
+
)
|
|
144
203
|
|
|
145
|
-
@async_handle_receiver_exceptions
|
|
146
204
|
async def async_post(
|
|
147
205
|
self,
|
|
148
206
|
request: str,
|
|
207
|
+
*,
|
|
149
208
|
content: Optional[bytes] = None,
|
|
150
209
|
data: Optional[Dict] = None,
|
|
151
210
|
port: Optional[int] = None,
|
|
211
|
+
cache_id: Hashable = None,
|
|
152
212
|
) -> httpx.Response:
|
|
153
213
|
"""Call POST endpoint of Denon AVR receiver asynchronously."""
|
|
154
214
|
# Use default port of the receiver if no different port is specified
|
|
@@ -156,20 +216,15 @@ class DenonAVRApi:
|
|
|
156
216
|
|
|
157
217
|
endpoint = f"http://{self.host}:{port}{request}"
|
|
158
218
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if self.is_default_async_client():
|
|
168
|
-
await client.aclose()
|
|
169
|
-
|
|
170
|
-
return res
|
|
219
|
+
return await self.httpx_async_client.async_post(
|
|
220
|
+
endpoint,
|
|
221
|
+
self.timeout,
|
|
222
|
+
self.read_timeout,
|
|
223
|
+
content=content,
|
|
224
|
+
data=data,
|
|
225
|
+
cache_id=cache_id,
|
|
226
|
+
)
|
|
171
227
|
|
|
172
|
-
@async_handle_receiver_exceptions
|
|
173
228
|
async def async_get_command(self, request: str) -> str:
|
|
174
229
|
"""Send HTTP GET command to Denon AVR receiver asynchronously."""
|
|
175
230
|
# HTTP GET to endpoint
|
|
@@ -177,34 +232,46 @@ class DenonAVRApi:
|
|
|
177
232
|
# Return text
|
|
178
233
|
return res.text
|
|
179
234
|
|
|
180
|
-
@cache_result
|
|
181
|
-
@async_handle_receiver_exceptions
|
|
182
235
|
async def async_get_xml(
|
|
183
|
-
self, request: str, cache_id: Hashable = None
|
|
236
|
+
self, request: str, *, cache_id: Hashable = None
|
|
184
237
|
) -> ET.Element:
|
|
185
238
|
"""Return XML data from HTTP GET endpoint asynchronously."""
|
|
186
239
|
# HTTP GET to endpoint
|
|
187
|
-
res = await self.async_get(request)
|
|
240
|
+
res = await self.async_get(request, cache_id=cache_id)
|
|
188
241
|
# create ElementTree
|
|
189
|
-
|
|
242
|
+
try:
|
|
243
|
+
xml_root = fromstring(res.text)
|
|
244
|
+
except (
|
|
245
|
+
ET.ParseError,
|
|
246
|
+
DefusedXmlException,
|
|
247
|
+
ParseError,
|
|
248
|
+
UnicodeDecodeError,
|
|
249
|
+
) as err:
|
|
250
|
+
raise AvrInvalidResponseError(f"XMLParseError: {err}", request) from err
|
|
190
251
|
# Check validity of XML
|
|
191
252
|
self.check_xml_validity(request, xml_root)
|
|
192
253
|
# Return ElementTree element
|
|
193
254
|
return xml_root
|
|
194
255
|
|
|
195
|
-
@cache_result
|
|
196
|
-
@async_handle_receiver_exceptions
|
|
197
256
|
async def async_post_appcommand(
|
|
198
|
-
self, request: str, cmds: Tuple[AppCommandCmd], cache_id: Hashable = None
|
|
257
|
+
self, request: str, cmds: Tuple[AppCommandCmd], *, cache_id: Hashable = None
|
|
199
258
|
) -> ET.Element:
|
|
200
259
|
"""Return XML from Appcommand(0300) endpoint asynchronously."""
|
|
201
260
|
# Prepare XML body for POST call
|
|
202
261
|
content = self.prepare_appcommand_body(cmds)
|
|
203
262
|
_LOGGER.debug("Content for %s endpoint: %s", request, content)
|
|
204
263
|
# HTTP POST to endpoint
|
|
205
|
-
res = await self.async_post(request, content=content)
|
|
264
|
+
res = await self.async_post(request, content=content, cache_id=cache_id)
|
|
206
265
|
# create ElementTree
|
|
207
|
-
|
|
266
|
+
try:
|
|
267
|
+
xml_root = fromstring(res.text)
|
|
268
|
+
except (
|
|
269
|
+
ET.ParseError,
|
|
270
|
+
DefusedXmlException,
|
|
271
|
+
ParseError,
|
|
272
|
+
UnicodeDecodeError,
|
|
273
|
+
) as err:
|
|
274
|
+
raise AvrInvalidResponseError(f"XMLParseError: {err}", request) from err
|
|
208
275
|
# Check validity of XML
|
|
209
276
|
self.check_xml_validity(request, xml_root)
|
|
210
277
|
# Add query tags to result
|
|
@@ -350,10 +417,6 @@ class DenonAVRApi:
|
|
|
350
417
|
|
|
351
418
|
return body_bytes
|
|
352
419
|
|
|
353
|
-
def is_default_async_client(self) -> bool:
|
|
354
|
-
"""Check if default httpx.AsyncClient getter is used."""
|
|
355
|
-
return self.async_client_getter is get_default_async_client
|
|
356
|
-
|
|
357
420
|
|
|
358
421
|
class DenonAVRTelnetProtocol(asyncio.Protocol):
|
|
359
422
|
"""Protocol for the Denon AVR Telnet interface."""
|
|
@@ -481,8 +544,8 @@ class DenonAVRTelnetApi:
|
|
|
481
544
|
"%s: Connection failed on telnet reconnect: %s", self.host, err
|
|
482
545
|
)
|
|
483
546
|
raise AvrNetworkError(f"OSError: {err}", "telnet connect") from err
|
|
484
|
-
_LOGGER.debug("%s: telnet connection established", self.host)
|
|
485
547
|
self._protocol = cast(DenonAVRTelnetProtocol, transport_protocol[1])
|
|
548
|
+
_LOGGER.debug("%s: telnet connection established", self.host)
|
|
486
549
|
self._connection_enabled = True
|
|
487
550
|
self._last_message_time = time.monotonic()
|
|
488
551
|
self._schedule_monitor()
|
|
@@ -543,7 +606,8 @@ class DenonAVRTelnetApi:
|
|
|
543
606
|
self._stop_monitor()
|
|
544
607
|
if not self._connection_enabled:
|
|
545
608
|
return
|
|
546
|
-
self._reconnect_task
|
|
609
|
+
if self._reconnect_task is None:
|
|
610
|
+
self._reconnect_task = asyncio.create_task(self._async_reconnect())
|
|
547
611
|
|
|
548
612
|
async def async_disconnect(self) -> None:
|
|
549
613
|
"""Close the connection to the receiver asynchronously."""
|
|
@@ -581,6 +645,12 @@ class DenonAVRTelnetApi:
|
|
|
581
645
|
)
|
|
582
646
|
except AvrNetworkError as err:
|
|
583
647
|
_LOGGER.debug("%s: %s", self.host, err)
|
|
648
|
+
except AvrProcessingError as err:
|
|
649
|
+
_LOGGER.debug(
|
|
650
|
+
"%s: Failed updating state on telnet reconnect: %s",
|
|
651
|
+
self.host,
|
|
652
|
+
err,
|
|
653
|
+
)
|
|
584
654
|
except Exception as err: # pylint: disable=broad-except
|
|
585
655
|
_LOGGER.error(
|
|
586
656
|
"%s: Unexpected exception on telnet reconnect",
|
|
@@ -589,11 +659,13 @@ class DenonAVRTelnetApi:
|
|
|
589
659
|
)
|
|
590
660
|
else:
|
|
591
661
|
_LOGGER.info("%s: Telnet reconnected", self.host)
|
|
592
|
-
|
|
662
|
+
break
|
|
593
663
|
|
|
594
664
|
await asyncio.sleep(backoff)
|
|
595
665
|
backoff = min(30.0, backoff * 2)
|
|
596
666
|
|
|
667
|
+
self._reconnect_task = None
|
|
668
|
+
|
|
597
669
|
def register_callback(
|
|
598
670
|
self, event: str, callback: Callable[[str, str, str], Awaitable[None]]
|
|
599
671
|
) -> None:
|
|
@@ -604,6 +676,8 @@ class DenonAVRTelnetApi:
|
|
|
604
676
|
|
|
605
677
|
if event not in self._callbacks.keys():
|
|
606
678
|
self._callbacks[event] = []
|
|
679
|
+
elif callback in self._callbacks[event]:
|
|
680
|
+
return
|
|
607
681
|
self._callbacks[event].append(callback)
|
|
608
682
|
|
|
609
683
|
def unregister_callback(
|
|
@@ -618,6 +692,8 @@ class DenonAVRTelnetApi:
|
|
|
618
692
|
self, callback: Callable[[str], Awaitable[None]]
|
|
619
693
|
) -> None:
|
|
620
694
|
"""Register a callback handler for raw telnet messages."""
|
|
695
|
+
if callback in self._raw_callbacks:
|
|
696
|
+
return
|
|
621
697
|
self._raw_callbacks.append(callback)
|
|
622
698
|
|
|
623
699
|
def _unregister_raw_callback(
|
|
@@ -10,14 +10,11 @@ This module implements the REST API to Denon AVR receivers.
|
|
|
10
10
|
import inspect
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
13
|
-
import xml.etree.ElementTree as ET
|
|
14
13
|
from functools import wraps
|
|
15
14
|
from typing import Callable, TypeVar
|
|
16
15
|
|
|
17
16
|
import httpx
|
|
18
17
|
from asyncstdlib import lru_cache
|
|
19
|
-
from defusedxml import DefusedXmlException
|
|
20
|
-
from defusedxml.ElementTree import ParseError
|
|
21
18
|
|
|
22
19
|
from .exceptions import (
|
|
23
20
|
AvrForbiddenError,
|
|
@@ -33,12 +30,7 @@ AnyT = TypeVar("AnyT")
|
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
def async_handle_receiver_exceptions(func: Callable[..., AnyT]) -> Callable[..., AnyT]:
|
|
36
|
-
"""
|
|
37
|
-
Handle exceptions raised when calling a Denon AVR endpoint asynchronously.
|
|
38
|
-
|
|
39
|
-
The decorated function must either have a string variable as second
|
|
40
|
-
argument or as "request" keyword argument.
|
|
41
|
-
"""
|
|
33
|
+
"""Handle exceptions raised when calling a Denon AVR endpoint asynchronously."""
|
|
42
34
|
|
|
43
35
|
@wraps(func)
|
|
44
36
|
async def wrapper(*args, **kwargs):
|
|
@@ -64,48 +56,29 @@ def async_handle_receiver_exceptions(func: Callable[..., AnyT]) -> Callable[...,
|
|
|
64
56
|
raise AvrInvalidResponseError(
|
|
65
57
|
f"RemoteProtocolError: {err}", err.request
|
|
66
58
|
) from err
|
|
67
|
-
except (
|
|
68
|
-
ET.ParseError,
|
|
69
|
-
DefusedXmlException,
|
|
70
|
-
ParseError,
|
|
71
|
-
UnicodeDecodeError,
|
|
72
|
-
) as err:
|
|
73
|
-
_LOGGER.debug(
|
|
74
|
-
"Defusedxml parse error on request %s: %s", (args, kwargs), err
|
|
75
|
-
)
|
|
76
|
-
raise AvrInvalidResponseError(
|
|
77
|
-
f"XMLParseError: {err}", (args, kwargs)
|
|
78
|
-
) from err
|
|
79
59
|
|
|
80
60
|
return wrapper
|
|
81
61
|
|
|
82
62
|
|
|
83
63
|
def cache_result(func: Callable[..., AnyT]) -> Callable[..., AnyT]:
|
|
84
64
|
"""
|
|
85
|
-
Decorate a function to cache its results with an lru_cache of maxsize
|
|
65
|
+
Decorate a function to cache its results with an lru_cache of maxsize 32.
|
|
86
66
|
|
|
87
67
|
This decorator also sets an "cache_id" keyword argument if it is not set yet.
|
|
88
|
-
When an exception occurs it clears lru_cache to prevent memory leaks in
|
|
89
|
-
home-assistant when receiver instances are created and deleted right
|
|
90
|
-
away in case the device is offline on setup.
|
|
91
68
|
"""
|
|
92
69
|
if inspect.signature(func).parameters.get("cache_id") is None:
|
|
93
70
|
raise AttributeError(
|
|
94
71
|
f"Function {func} does not have a 'cache_id' keyword parameter"
|
|
95
72
|
)
|
|
96
73
|
|
|
97
|
-
lru_decorator = lru_cache(maxsize=
|
|
74
|
+
lru_decorator = lru_cache(maxsize=32)
|
|
98
75
|
cached_func = lru_decorator(func)
|
|
99
76
|
|
|
100
77
|
@wraps(func)
|
|
101
78
|
async def wrapper(*args, **kwargs):
|
|
102
79
|
if kwargs.get("cache_id") is None:
|
|
103
80
|
kwargs["cache_id"] = time.time()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
except Exception as err:
|
|
107
|
-
_LOGGER.debug("Exception raised, clearing cache: %s", err)
|
|
108
|
-
cached_func.cache_clear()
|
|
109
|
-
raise
|
|
81
|
+
|
|
82
|
+
return await cached_func(*args, **kwargs)
|
|
110
83
|
|
|
111
84
|
return wrapper
|
|
@@ -519,7 +519,7 @@ class DenonAVR(DenonAVRFoundation):
|
|
|
519
519
|
"""
|
|
520
520
|
if not callable(async_client_getter):
|
|
521
521
|
raise AvrCommandError("Provided object is not callable")
|
|
522
|
-
self._device.api.
|
|
522
|
+
self._device.api.httpx_async_client.client_getter = async_client_getter
|
|
523
523
|
|
|
524
524
|
async def async_dynamic_eq_off(self) -> None:
|
|
525
525
|
"""Turn DynamicEQ off."""
|
|
@@ -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
|
|
@@ -146,15 +145,15 @@ class DenonAVRDeviceInfo:
|
|
|
146
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
|
-
|
|
150
|
-
self.api.
|
|
148
|
+
read_timeout = self.api.read_timeout
|
|
149
|
+
self.api.read_timeout = self.api.timeout
|
|
151
150
|
try:
|
|
152
151
|
_LOGGER.debug("Identifying receiver")
|
|
153
152
|
await self.async_identify_receiver()
|
|
154
153
|
_LOGGER.debug("Getting device info")
|
|
155
154
|
await self.async_get_device_info()
|
|
156
155
|
finally:
|
|
157
|
-
self.api.
|
|
156
|
+
self.api.read_timeout = read_timeout
|
|
158
157
|
_LOGGER.debug("Identifying update method")
|
|
159
158
|
await self.async_identify_update_method()
|
|
160
159
|
|
|
@@ -323,7 +322,7 @@ class DenonAVRDeviceInfo:
|
|
|
323
322
|
self._set_friendly_name(xml)
|
|
324
323
|
|
|
325
324
|
async def async_verify_avr_2016_update_method(
|
|
326
|
-
self, cache_id: Hashable = None
|
|
325
|
+
self, *, cache_id: Hashable = None
|
|
327
326
|
) -> None:
|
|
328
327
|
"""Verify if avr 2016 update method is working."""
|
|
329
328
|
# Nothing to do if Appcommand.xml interface is not supported
|
|
@@ -833,9 +832,9 @@ def set_api_timeout(
|
|
|
833
832
|
) -> float:
|
|
834
833
|
"""Change API timeout on timeout changes too."""
|
|
835
834
|
# First change _device.api.host then return value
|
|
836
|
-
timeout = httpx.Timeout(value, read=max(value, 15.0))
|
|
837
835
|
# pylint: disable=protected-access
|
|
838
|
-
instance._device.api.timeout =
|
|
836
|
+
instance._device.api.timeout = value
|
|
837
|
+
instance._device.api.read_timeout = max(value, 15.0)
|
|
839
838
|
instance._device.telnet_api.timeout = value
|
|
840
839
|
return value
|
|
841
840
|
|
|
@@ -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}
|
|
@@ -516,6 +523,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
516
523
|
except AttributeError:
|
|
517
524
|
continue
|
|
518
525
|
|
|
526
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
527
|
+
|
|
519
528
|
return (renamed_sources, deleted_sources)
|
|
520
529
|
|
|
521
530
|
async def async_get_changed_sources_status_xml(
|
|
@@ -604,6 +613,8 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
604
613
|
except IndexError:
|
|
605
614
|
_LOGGER.error("List of deleted sources incomplete, continuing anyway")
|
|
606
615
|
|
|
616
|
+
self._replace_duplicate_sources(renamed_sources)
|
|
617
|
+
|
|
607
618
|
return (renamed_sources, deleted_sources)
|
|
608
619
|
|
|
609
620
|
async def async_update_inputfuncs(
|
|
@@ -859,10 +870,13 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
859
870
|
async def _async_test_image_accessible(self) -> None:
|
|
860
871
|
"""Test if image URL is accessible."""
|
|
861
872
|
if self._image_available is None and self._image_url is not None:
|
|
862
|
-
client = self._device.api.
|
|
873
|
+
client = self._device.api.httpx_async_client.client_getter()
|
|
863
874
|
try:
|
|
864
875
|
res = await client.get(
|
|
865
|
-
self._image_url,
|
|
876
|
+
self._image_url,
|
|
877
|
+
timeout=httpx.Timeout(
|
|
878
|
+
self._device.api.timeout, read=self._device.api.read_timeout
|
|
879
|
+
),
|
|
866
880
|
)
|
|
867
881
|
res.raise_for_status()
|
|
868
882
|
except httpx.TimeoutException:
|
|
@@ -878,7 +892,7 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
878
892
|
self._image_available = True
|
|
879
893
|
finally:
|
|
880
894
|
# Close the default AsyncClient but keep custom clients open
|
|
881
|
-
if self._device.api.is_default_async_client():
|
|
895
|
+
if self._device.api.httpx_async_client.is_default_async_client():
|
|
882
896
|
await client.aclose()
|
|
883
897
|
# Already tested that image URL is not accessible
|
|
884
898
|
elif not self._image_available:
|
|
@@ -894,6 +908,33 @@ class DenonAVRInput(DenonAVRFoundation):
|
|
|
894
908
|
self._station = None
|
|
895
909
|
self._image_url = None
|
|
896
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
|
+
|
|
897
938
|
##############
|
|
898
939
|
# Properties #
|
|
899
940
|
##############
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "denonavr"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
authors = [{name = "Oliver Goetz", email = "scarface@mywoh.de"}]
|
|
9
9
|
license = {text = "MIT"}
|
|
10
10
|
description = "Automation Library for Denon AVR receivers"
|
|
@@ -153,6 +153,7 @@ class TestMainFunctions:
|
|
|
153
153
|
self.future.set_result(True)
|
|
154
154
|
|
|
155
155
|
@pytest.mark.asyncio
|
|
156
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
156
157
|
async def test_receiver_type(self, httpx_mock: HTTPXMock):
|
|
157
158
|
"""Check that receiver type is determined correctly."""
|
|
158
159
|
httpx_mock.add_callback(self.custom_matcher)
|
|
@@ -172,6 +173,7 @@ class TestMainFunctions:
|
|
|
172
173
|
)
|
|
173
174
|
|
|
174
175
|
@pytest.mark.asyncio
|
|
176
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
175
177
|
async def test_input_func_switch(self, httpx_mock: HTTPXMock):
|
|
176
178
|
"""Switch through all input functions of all tested receivers."""
|
|
177
179
|
httpx_mock.add_callback(self.custom_matcher)
|
|
@@ -188,6 +190,7 @@ class TestMainFunctions:
|
|
|
188
190
|
await self.denon.zones[name].async_set_input_func(input_func)
|
|
189
191
|
|
|
190
192
|
@pytest.mark.asyncio
|
|
193
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
191
194
|
async def test_attributes_not_none(self, httpx_mock: HTTPXMock):
|
|
192
195
|
"""Check that certain attributes are not None."""
|
|
193
196
|
httpx_mock.add_callback(self.custom_matcher)
|
|
@@ -210,6 +213,7 @@ class TestMainFunctions:
|
|
|
210
213
|
), f"State is None for receiver {receiver}"
|
|
211
214
|
|
|
212
215
|
@pytest.mark.asyncio
|
|
216
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
213
217
|
async def test_sound_mode(self, httpx_mock: HTTPXMock):
|
|
214
218
|
"""Check if a valid sound mode is returned."""
|
|
215
219
|
httpx_mock.add_callback(self.custom_matcher)
|
|
@@ -393,6 +397,7 @@ class TestMainFunctions:
|
|
|
393
397
|
await api.async_connect()
|
|
394
398
|
|
|
395
399
|
@pytest.mark.asyncio
|
|
400
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
396
401
|
async def test_receive_callback_called(self, httpx_mock: HTTPXMock):
|
|
397
402
|
"""Check that the callback is triggered whena message is received."""
|
|
398
403
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -428,6 +433,7 @@ class TestMainFunctions:
|
|
|
428
433
|
mock_obj.method.assert_called_once()
|
|
429
434
|
|
|
430
435
|
@pytest.mark.asyncio
|
|
436
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
431
437
|
async def test_mute_on(self, httpx_mock: HTTPXMock):
|
|
432
438
|
"""Check that mute on is processed."""
|
|
433
439
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -458,6 +464,7 @@ class TestMainFunctions:
|
|
|
458
464
|
assert self.denon.muted
|
|
459
465
|
|
|
460
466
|
@pytest.mark.asyncio
|
|
467
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
461
468
|
async def test_mute_off(self, httpx_mock: HTTPXMock):
|
|
462
469
|
"""Check that mute off is processed."""
|
|
463
470
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -488,6 +495,7 @@ class TestMainFunctions:
|
|
|
488
495
|
assert not self.denon.muted
|
|
489
496
|
|
|
490
497
|
@pytest.mark.asyncio
|
|
498
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
491
499
|
async def test_power_on(self, httpx_mock: HTTPXMock):
|
|
492
500
|
"""Check that power on is processed."""
|
|
493
501
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -518,6 +526,7 @@ class TestMainFunctions:
|
|
|
518
526
|
assert self.denon.power == "ON"
|
|
519
527
|
|
|
520
528
|
@pytest.mark.asyncio
|
|
529
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
521
530
|
async def test_power_off(self, httpx_mock: HTTPXMock):
|
|
522
531
|
"""Check that power off is processed."""
|
|
523
532
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -548,6 +557,7 @@ class TestMainFunctions:
|
|
|
548
557
|
assert self.denon.power == "OFF"
|
|
549
558
|
|
|
550
559
|
@pytest.mark.asyncio
|
|
560
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
551
561
|
async def test_volume_min(self, httpx_mock: HTTPXMock):
|
|
552
562
|
"""Check that minimum volume is processed."""
|
|
553
563
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -578,6 +588,7 @@ class TestMainFunctions:
|
|
|
578
588
|
assert self.denon.volume == -80.0
|
|
579
589
|
|
|
580
590
|
@pytest.mark.asyncio
|
|
591
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
581
592
|
async def test_volume_wholenumber(self, httpx_mock: HTTPXMock):
|
|
582
593
|
"""Check that whole number volume is processed."""
|
|
583
594
|
transport = mock.Mock(is_closing=lambda: False)
|
|
@@ -608,6 +619,7 @@ class TestMainFunctions:
|
|
|
608
619
|
assert self.denon.volume == -24.0
|
|
609
620
|
|
|
610
621
|
@pytest.mark.asyncio
|
|
622
|
+
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
|
|
611
623
|
async def test_volume_fraction(self, httpx_mock: HTTPXMock):
|
|
612
624
|
"""Check that fractional volume is processed."""
|
|
613
625
|
transport = mock.Mock(is_closing=lambda: False)
|
|
File without changes
|