From 72294567cd35904e7b5ee68595f577eea603ce66 Mon Sep 17 00:00:00 2001 From: ol-iver Date: Thu, 28 Nov 2024 23:09:43 +0100 Subject: [PATCH 01/13] Create new dev version 1.0.2-dev --- denonavr/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/denonavr/__init__.py b/denonavr/__init__.py index 36a6ab1..8b8f4e0 100644 --- a/denonavr/__init__.py +++ b/denonavr/__init__.py @@ -17,7 +17,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) __title__ = "denonavr" -__version__ = "1.0.1" +__version__ = "1.0.2-dev" async def async_discover(): diff --git a/pyproject.toml b/pyproject.toml index da9a5b9..e6b3354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "denonavr" -version = "1.0.1" +version = "1.0.2-dev" authors = [{name = "Oliver Goetz", email = "scarface@mywoh.de"}] license = {text = "MIT"} description = "Automation Library for Denon AVR receivers" From 9f77196202dd8762dcb30d19b627b27e1edb56ad Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:11:36 +0100 Subject: [PATCH 02/13] Skip confirmation for commands which don't return anything (#325) * Skip confirmation for commands which don't return anything #321 * PR feedback and skip confirmation for async_back --- denonavr/foundation.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/denonavr/foundation.py b/denonavr/foundation.py index 4c3ff0b..fac7ca3 100644 --- a/denonavr/foundation.py +++ b/denonavr/foundation.py @@ -552,7 +552,7 @@ async def async_cursor_up(self) -> None: """Cursor Up on receiver via HTTP get command.""" if self.telnet_available: await self.telnet_api.async_send_commands( - self.telnet_commands.command_cusor_up + self.telnet_commands.command_cusor_up, skip_confirmation=True ) else: await self.api.async_get_command(self.urls.command_cusor_up) @@ -561,7 +561,7 @@ async def async_cursor_down(self) -> None: """Cursor Down on receiver via HTTP get command.""" if self.telnet_available: await self.telnet_api.async_send_commands( - self.telnet_commands.command_cusor_down + self.telnet_commands.command_cusor_down, skip_confirmation=True ) else: await self.api.async_get_command(self.urls.command_cusor_down) @@ -570,7 +570,7 @@ async def async_cursor_left(self) -> None: """Cursor Left on receiver via HTTP get command.""" if self.telnet_available: await self.telnet_api.async_send_commands( - self.telnet_commands.command_cusor_left + self.telnet_commands.command_cusor_left, skip_confirmation=True ) else: await self.api.async_get_command(self.urls.command_cusor_left) @@ -579,7 +579,7 @@ async def async_cursor_right(self) -> None: """Cursor Right on receiver via HTTP get command.""" if self.telnet_available: await self.telnet_api.async_send_commands( - self.telnet_commands.command_cusor_right + self.telnet_commands.command_cusor_right, skip_confirmation=True ) else: await self.api.async_get_command(self.urls.command_cusor_right) @@ -588,7 +588,7 @@ async def async_cursor_enter(self) -> None: """Cursor Enter on receiver via HTTP get command.""" if self.telnet_available: await self.telnet_api.async_send_commands( - self.telnet_commands.command_cusor_enter + self.telnet_commands.command_cusor_enter, skip_confirmation=True ) else: await self.api.async_get_command(self.urls.command_cusor_enter) @@ -596,20 +596,29 @@ async def async_cursor_enter(self) -> None: async def async_back(self) -> None: """Back command on receiver via HTTP get command.""" if self.telnet_available: - await self.telnet_api.async_send_commands(self.telnet_commands.command_back) + await self.telnet_api.async_send_commands( + self.telnet_commands.command_back, skip_confirmation=True + ) else: await self.api.async_get_command(self.urls.command_back) async def async_info(self) -> None: """Info OSD on receiver via HTTP get command.""" if self.telnet_available: - await self.telnet_api.async_send_commands(self.telnet_commands.command_info) + await self.telnet_api.async_send_commands( + self.telnet_commands.command_info, skip_confirmation=True + ) else: await self.api.async_get_command(self.urls.command_info) async def async_options(self) -> None: """Options menu on receiver via HTTP get command.""" - await self.api.async_get_command(self.urls.command_options) + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_options, skip_confirmation=True + ) + else: + await self.api.async_get_command(self.urls.command_options) async def async_settings_menu(self) -> None: """Options menu on receiver via HTTP get command.""" From 892b916b0e9ab3729486655aed96dad1a555e12d Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:51:39 +0100 Subject: [PATCH 03/13] Add support for more sound modes (#324) * Add support for more sound modes Sound modes fetched from the AVC-A10H specification * Add alternative without typo as official docs has the typo --- denonavr/const.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/denonavr/const.py b/denonavr/const.py index eeee797..9d39d4a 100644 --- a/denonavr/const.py +++ b/denonavr/const.py @@ -276,6 +276,11 @@ "MULTI IN + NEURAL:X", "NEURAL", "STANDARD(DOLBY)", + "DOLBY AUDIO-DD+NEURAL:X", + "DOLBY AUDIO-DD+", + # Added both variants as we don't know if type is intentional + "DOLBY AUDIO-DD+ +NERUAL:X", + "DOLBY AUDIO-DD+ +NEURAL:X", ], "DTS SURROUND": [ "DTS + DOLBY SURROUND", @@ -296,6 +301,21 @@ "NEURAL:X", "STANDARD(DTS)", "VIRTUAL:X", + "IMAX DTS", + "IMAX DTS+NEURAL:X", + "MAX DTS+VIRTUAL:X", + "DTS+DSUR", + "DTS+NEURAL:X", + "DTS+VIRTUAL:X", + "DTS HD", + "DTS HD+DSUR", + "DTS HD+NEURAL:X", + "DTS HD+VIRTUAL:X", + "DTS:X+VIRTUAL:X", + "IMAX DTS:X", + "IMAX DTS:X+VIRTUAL:X", + "M CH IN+NEURAL:X", + "M CH IN+VIRTUAL:X", ], "AURO3D": ["AURO-3D", "AURO3D"], "AURO2DSURR": ["AURO-2D SURROUND", "AURO2DSURR"], From b9cf9664916ad3a363c8c7ec1f389aee97410d73 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:10:23 +0100 Subject: [PATCH 04/13] Remove python 3.7 support (#326) --- .github/workflows/python-tests.yml | 2 +- pyproject.toml | 3 +-- tests/test_denonavr.py | 37 +++++++++++++----------------- tox.ini | 2 +- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 0cb20a1..3a8c44d 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] name: Testing Python ${{ matrix.python-version }} steps: diff --git a/pyproject.toml b/pyproject.toml index e6b3354..770cf85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", "Topic :: Home Automation", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -23,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "asyncstdlib>=3.10.2", "attrs>=21.2.0", diff --git a/tests/test_denonavr.py b/tests/test_denonavr.py index ca3c444..d234976 100644 --- a/tests/test_denonavr.py +++ b/tests/test_denonavr.py @@ -361,7 +361,7 @@ async def test_connect_connectionrefused_raises_networkerror(self): """Connect raises NetworkError when ConnectionRefused.""" api = DenonAVRTelnetApi() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=ConnectionRefusedError() ) with pytest.raises(AvrNetworkError): @@ -372,7 +372,9 @@ async def test_connect_oserror_raises_networkerror(self): """Connect raises NetworkError when OSError.""" api = DenonAVRTelnetApi() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock(side_effect=OSError()) + debug_mock.return_value.create_connection = mock.AsyncMock( + side_effect=OSError() + ) with pytest.raises(AvrNetworkError): await api.async_connect() @@ -381,7 +383,9 @@ async def test_connect_ioerror_raises_networkerror(self): """Connect raises NetworkError when IOError.""" api = DenonAVRTelnetApi() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock(side_effect=IOError()) + debug_mock.return_value.create_connection = mock.AsyncMock( + side_effect=IOError() + ) with pytest.raises(AvrNetworkError): await api.async_connect() @@ -390,7 +394,7 @@ async def test_connect_timeouterror_raises_timeouterror(self): """Connect raises AvrTimeoutError when TimeoutError.""" api = DenonAVRTelnetApi() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=asyncio.TimeoutError() ) with pytest.raises(AvrTimoutError): @@ -420,7 +424,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -453,7 +457,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -484,7 +488,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -515,7 +519,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -546,7 +550,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -577,7 +581,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -608,7 +612,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -639,7 +643,7 @@ def create_conn(proto_lambda, host, port): self.denon._device.telnet_api._send_confirmation_timeout = 0.1 await self.denon.async_setup() with mock.patch("asyncio.get_event_loop", new_callable=mock.Mock) as debug_mock: - debug_mock.return_value.create_connection = AsyncMock( + debug_mock.return_value.create_connection = mock.AsyncMock( side_effect=create_conn ) await self.denon.async_telnet_connect() @@ -648,12 +652,3 @@ def create_conn(proto_lambda, host, port): protocol.data_received(b"MV565\r") await self.future assert self.denon.volume == -23.5 - - -class AsyncMock(mock.MagicMock): - """Mocking async methods compatible to python 3.7.""" - - # pylint: disable=invalid-overridden-method,useless-super-delegation - async def __call__(self, *args, **kwargs): - """Call.""" - return super().__call__(*args, **kwargs) diff --git a/tox.ini b/tox.ini index cd80c93..f2179ea 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,py312,py313,pylint,lint,format +envlist = py38,py39,py310,py311,py312,py313,pylint,lint,format skip_missing_interpreters = True [testenv:format] From 0e4f0338a6865759db7c9b110a2e59a1e1fb0dd9 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:19:29 +0100 Subject: [PATCH 05/13] Add dependabot (#327) --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..51a48e1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: "daily" + assignees: + - "ol-iver" + reviewers: + - "ol-iver" From 75e4f595436948591c84ca9113899914caab4b33 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Sat, 22 Mar 2025 18:57:46 +0100 Subject: [PATCH 06/13] Add settings menu state (#328) * Add settings menu state Allows for settings menu toggle without relying on HTTP * PR feedback * Additional PR feedback --- denonavr/api.py | 1 + denonavr/const.py | 3 +++ denonavr/denonavr.py | 9 +++++++++ denonavr/foundation.py | 40 ++++++++++++++++++++++++++++++++++------ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/denonavr/api.py b/denonavr/api.py index c6d17a5..98b0ce0 100644 --- a/denonavr/api.py +++ b/denonavr/api.py @@ -567,6 +567,7 @@ async def _async_establish_connection(self) -> None: "PSREFLEV ?", "PSDYNVOL ?", "MS?", + "MNMEN?", skip_confirmation=True, ) diff --git a/denonavr/const.py b/denonavr/const.py index 9d39d4a..f42c570 100644 --- a/denonavr/const.py +++ b/denonavr/const.py @@ -689,6 +689,9 @@ STATE_OFF = "off" STATE_PLAYING = "playing" STATE_PAUSED = "paused" +SETTINGS_MENU_ON = "ON" +SETTINGS_MENU_OFF = "OFF" +SETTINGS_MENU_STATES = {SETTINGS_MENU_ON, SETTINGS_MENU_OFF} # Zones ALL_ZONES = "All" diff --git a/denonavr/denonavr.py b/denonavr/denonavr.py index 904c162..f15ea30 100644 --- a/denonavr/denonavr.py +++ b/denonavr/denonavr.py @@ -278,6 +278,15 @@ def power(self) -> Optional[str]: """ return self._device.power + @property + def settings_menu(self) -> Optional[str]: + """ + Return the settings menu state of the device. Only available if using Telnet. + + Possible values are: "ON" and "OFF" + """ + return self._device.settings_menu + @property def state(self) -> Optional[str]: """ diff --git a/denonavr/foundation.py b/denonavr/foundation.py index fac7ca3..cf3119b 100644 --- a/denonavr/foundation.py +++ b/denonavr/foundation.py @@ -32,6 +32,7 @@ DEVICEINFO_COMMAPI_PATTERN, MAIN_ZONE, POWER_STATES, + SETTINGS_MENU_STATES, VALID_RECEIVER_TYPES, VALID_ZONES, ZONE2, @@ -102,6 +103,9 @@ class DenonAVRDeviceInfo: _power: Optional[str] = attr.ib( converter=attr.converters.optional(str), default=None ) + _settings_menu: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) _is_setup: bool = attr.ib(converter=bool, default=False, init=False) _allow_recovery: bool = attr.ib(converter=bool, default=True, init=True) _setup_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock)) @@ -128,6 +132,17 @@ async def _async_power_callback( if self.zone == zone and parameter in POWER_STATES: self._power = parameter + async def _async_settings_menu_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a settings menu event.""" + if ( + event == "MN" + and parameter[0:3] == "MEN" + and parameter[4:] in SETTINGS_MENU_STATES + ): + self._settings_menu = parameter[4:] + def get_own_zone(self) -> str: """ Get zone from actual instance. @@ -167,6 +182,8 @@ async def async_setup(self) -> None: power_event = "Z3" self.telnet_api.register_callback(power_event, self._async_power_callback) + self.telnet_api.register_callback("MN", self._async_settings_menu_callback) + self._is_setup = True _LOGGER.debug("Finished device setup") @@ -521,6 +538,15 @@ def power(self) -> Optional[str]: """ return self._power + @property + def settings_menu(self) -> Optional[str]: + """ + Return the settings menu state of the device. Only available if using Telnet. + + Possible values are: "ON" and "OFF" + """ + return self._settings_menu + @property def telnet_available(self) -> bool: """Return true if telnet is connected and healthy.""" @@ -622,9 +648,9 @@ async def async_options(self) -> None: async def async_settings_menu(self) -> None: """Options menu on receiver via HTTP get command.""" - res = await self.api.async_get_command(self.urls.command_setup_query) + # fast path if we already know the state if self.telnet_available: - if res is not None and res == "MNMEN ON": + if self._settings_menu == "ON": await self.telnet_api.async_send_commands( self.telnet_commands.command_setup_close ) @@ -632,11 +658,13 @@ async def async_settings_menu(self) -> None: await self.telnet_api.async_send_commands( self.telnet_commands.command_setup_open ) + return + # slow path if we need to query the state + res = await self.api.async_get_command(self.urls.command_setup_query) + if res is not None and res == "MNMEN ON": + await self.api.async_get_command(self.urls.command_setup_close) else: - if res is not None and res == "MNMEN ON": - await self.api.async_get_command(self.urls.command_setup_close) - else: - await self.api.async_get_command(self.urls.command_setup_open) + await self.api.async_get_command(self.urls.command_setup_open) @attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR) From af943b97476df1d2aba4e5e4a5f8739e28b403b1 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Thu, 1 May 2025 17:41:06 +0200 Subject: [PATCH 07/13] Add support for more commands (#323) * Add support for more commands Commands added: - Dimmer Control - Channel Level Control - Audio Delay Control - Dirac Filter Control - Eco Mode Control - Audyssey LFC Control - HDMI Output Control - Status Display - Surround Mode Next/Previous * Fix formatting * Fix linter errors * Fix linter errors again * Fix linter errors again, again * More lint * :O * :O * Replace literals with enums as python 3.7 doesn't support it * Copilot auto-complete * There's always something :) * There's always something more :) * Add back literals * Add back literals * Remove enums and use mappers * Fix casing * Linting * Add mapping tests * Add missing validation * Move Dirac literal * Move Dirac literal, correct method name and add method to denonavr * Make the filter values match what's displayed on the unit + add tests * Missed casing * Add property for LFC and correct identifiers * Listen to events and set properties for: - Channel levels - Delay - Dimmer - Dirac filter - Eco mode - HDMI output * Linting * Linting * Linting * Convert values from API to dB I have no idea on how to convert the API values, so there's just a dumb mapper * Rename channel level to channel volume and move to volume * Add support for IMAX and Neural:X sound mode controls * Subwoofer control * Tactile Transducer control * Remove debug logging * Linting * Support for quick select * Channel level adjust menu * Auto standby * Sleep * HDMI Audio Decode * More consistent naming * Center Spread * More IMAX settings * Fix incorrect urls * Video Processing Modes * LFE * Loudness Management * Add missing comments * Cleanup * Bass Sync * Dialog Enhancer * Allow setting channel levels directly * Cinema EQ * Auro * More Audyssey * Room Size * System reset and Network reboot * Return response from async_status * Triggers * Speaker preset * Bluetooth * Remove logging * Dialog control * Speaker Virtualizer * Effect Speaker Selection * DRC * Delay Time * Audio Restorer * Remote and Panel lock * Correct method name * Python 3.8 compatability * Cleanup * Fix issue with init of dictionary * Restrict standby to main zone * Headphone EQ * Graphic EQ * Use bool instead of ON/OFF string * Settings menu toggle doesn't work in HTTP mode, remove the code for it * Document valid presets --- denonavr/api.py | 44 ++ denonavr/audyssey.py | 105 ++++ denonavr/const.py | 838 ++++++++++++++++++++++++++++ denonavr/denonavr.py | 500 ++++++++++++++++- denonavr/dirac.py | 89 +++ denonavr/foundation.py | 1168 +++++++++++++++++++++++++++++++++++++++- denonavr/soundmode.py | 915 ++++++++++++++++++++++++++++++- denonavr/volume.py | 407 +++++++++++++- tests/test_const.py | 57 ++ 9 files changed, 4090 insertions(+), 33 deletions(-) create mode 100644 denonavr/dirac.py create mode 100644 tests/test_const.py diff --git a/denonavr/api.py b/denonavr/api.py index 98b0ce0..cde5d9e 100644 --- a/denonavr/api.py +++ b/denonavr/api.py @@ -563,11 +563,55 @@ async def _async_establish_connection(self) -> None: "PSBAS ?", "PSTRE ?", "PSDYNEQ ?", + "PSLFC ?", + "PSCNTAMT ?", "PSMULTEQ: ?", "PSREFLEV ?", "PSDYNVOL ?", "MS?", "MNMEN?", + "DIM ?", + "PSDELAY?", + "ECO?", + "VSMONI ?", + "PSDIRAC ?", + "CV?", + "PSNEURAL ?", + "PSIMAX ?", + "PSIMAXAUD ?", + "PSIMAXHPF ?", + "PSIMAXLPF ?", + "PSIMAXSWM ?", + "PSIMAXSWO ?", + "PSSWR ?", + "PSSWL ?", + "SSTTR ?", + "MSQUICK ?", + "STBY?", + "SLP?", + "VSAUDIO ?", + "PSCES ?", + "VSVPM ?", + "PSLFE ?", + "PSLOM ?", + "PSBSC ?", + "PSDEH ?", + "PSCINEMA EQ. ?", + "PSAUROPR ?", + "PSAUROST ?", + "PSAUROMODE ?", + "PSRSZ ?", + "TR?", + "SPPR ?", + "BTTX ?", + "PSDIC ?", + "PSSPV ?", + "PSSP: ?", + "PSDRC ?", + "PSDEL ?", + "PSRSTR ?", + "PSGEQ ?", + "PSHEQ ?", skip_confirmation=True, ) diff --git a/denonavr/audyssey.py b/denonavr/audyssey.py index 219b457..bb372e6 100644 --- a/denonavr/audyssey.py +++ b/denonavr/audyssey.py @@ -60,6 +60,12 @@ class DenonAVRAudyssey(DenonAVRFoundation): _multeq_control: Optional[bool] = attr.ib( converter=attr.converters.optional(convert_string_int_bool), default=None ) + _lfc: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_string_int_bool), default=None + ) + _containment_amount: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) # Update tags for attributes # AppCommand0300.xml interface @@ -94,6 +100,12 @@ async def _async_sound_detail_callback( self._dynamiceq = "1" elif parameter == "DYNEQ OFF": self._dynamiceq = "0" + elif parameter == "LFC ON": + self._lfc = "1" + elif parameter == "LFC OFF": + self._lfc = "0" + elif parameter[:6] == "CNTAMT": + self._containment_amount = int(parameter[7:]) async def async_update( self, global_update: bool = False, cache_id: Optional[Hashable] = None @@ -186,6 +198,24 @@ def multi_eq_setting_list(self) -> List[str]: return list(MULTI_EQ_MAP_LABELS_TELNET.keys()) return list(MULTI_EQ_MAP_LABELS_APPCOMMAND.keys()) + @property + def lfc(self) -> Optional[bool]: + """ + Return value of LFC. + + Only available if using Telnet. + """ + return self._lfc + + @property + def containment_amount(self) -> Optional[int]: + """ + Return value of Containment Amount. + + Only available if using Telnet. + """ + return self._containment_amount + ########## # Setter # ########## @@ -284,6 +314,81 @@ async def async_toggle_dynamic_eq(self) -> None: else: await self.async_dynamiceq_on() + async def async_lfc_on(self): + """Turn LFC on.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_lfc.format(mode="ON") + ) + return + await self._device.api.async_get_command( + self._device.urls.command_lfc.format(mode="ON") + ) + + async def async_lfc_off(self): + """Turn LFC off.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_lfc.format(mode="OFF") + ) + return + await self._device.api.async_get_command( + self._device.urls.command_lfc.format(mode="OFF") + ) + + async def async_toggle_lfc(self): + """Toggle LFC.""" + if self._lfc: + await self.async_lfc_off() + else: + await self.async_lfc_on() + + async def async_containment_amount(self, amount: int) -> None: + """ + Set Containment Amount. + + Valid values are 1-7. + """ + if amount < 1 or amount > 7: + raise AvrCommandError("Containment amount must be between 1 and 7") + local_amount = f"{amount:02}" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_containment_amount.format( + value=local_amount + ) + ) + return + await self._device.api.async_get_command( + self._device.urls.command_containment_amount.format(value=local_amount) + ) + + async def async_containment_amount_up(self) -> None: + """Increase Containment Amount.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_containment_amount.format( + value="UP" + ) + ) + return + await self._device.api.async_get_command( + self._device.urls.command_containment_amount.format(value="UP") + ) + + async def async_containment_amount_down(self) -> None: + """Decrease Containment Amount.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_containment_amount.format( + value="DOWN" + ) + ) + return + await self._device.api.async_get_command( + self._device.urls.command_containment_amount.format(value="DOWN") + ) + def audyssey_factory(instance: DenonAVRFoundation) -> DenonAVRAudyssey: """Create DenonAVRAudyssey at receiver instances.""" diff --git a/denonavr/const.py b/denonavr/const.py index f42c570..aaeff5d 100644 --- a/denonavr/const.py +++ b/denonavr/const.py @@ -9,6 +9,7 @@ import re from collections import namedtuple +from typing import Literal import attr @@ -39,6 +40,14 @@ "command_mute_on", "command_mute_off", "command_sel_sound_mode", + "command_neural_x_on_off", + "command_imax_auto_off", + "command_imax_audio_settings", + "command_imax_hpf", + "command_imax_lpf", + "command_imax_subwoofer_mode", + "command_imax_subwoofer_output", + "command_cinema_eq", "command_netaudio_post", "command_set_all_zone_stereo", "command_pause", @@ -54,6 +63,55 @@ "command_setup_open", "command_setup_close", "command_setup_query", + "command_channel_level_adjust", + "command_dimmer_toggle", + "command_dimmer_set", + "command_channel_volume", + "command_channel_volumes_reset", + "command_subwoofer_on_off", + "command_subwoofer_level", + "command_lfe", + "command_tactile_transducer", + "command_tactile_transducer_level", + "command_tactile_transducer_lpf", + "command_delay_up", + "command_delay_down", + "command_auromatic_3d_preset", + "command_auromatic_3d_strength", + "command_auro_3d_mode", + "command_dirac_filter", + "command_eco_mode", + "command_lfc", + "command_containment_amount", + "command_loudness_management", + "command_bass_sync", + "command_dialog_enhancer", + "command_hdmi_output", + "command_hdmi_audio_decode", + "command_quick_select_mode", + "command_quick_select_memory", + "command_auto_standby", + "command_sleep", + "command_center_spread", + "command_video_processing_mode", + "command_room_size", + "command_status", + "command_system_reset", + "command_network_restart", + "command_trigger", + "command_speaker_preset", + "command_bluetooth_transmitter", + "command_dialog_control", + "command_speaker_virtualizer", + "command_effect_speaker_selection", + "command_drc", + "command_delay_time", + "command_audio_restorer", + "command_remote_control_lock", + "command_panel_lock", + "command_panel_and_volume_lock", + "command_graphic_eq", + "command_headphone_eq", ], ) TelnetCommands = namedtuple( @@ -69,6 +127,14 @@ "command_mute_on", "command_mute_off", "command_sel_sound_mode", + "command_neural_x_on_off", + "command_imax_auto_off", + "command_imax_audio_settings", + "command_imax_hpf", + "command_imax_lpf", + "command_imax_subwoofer_mode", + "command_imax_subwoofer_output", + "command_cinema_eq", "command_set_all_zone_stereo", "command_pause", "command_play", @@ -90,6 +156,55 @@ "command_setup_open", "command_setup_close", "command_setup_query", + "command_channel_level_adjust", + "command_dimmer_toggle", + "command_dimmer_set", + "command_channel_volume", + "command_channel_volumes_reset", + "command_subwoofer_on_off", + "command_subwoofer_level", + "command_lfe", + "command_tactile_transducer", + "command_tactile_transducer_level", + "command_tactile_transducer_lpf", + "command_delay_up", + "command_delay_down", + "command_auromatic_3d_preset", + "command_auromatic_3d_strength", + "command_auro_3d_mode", + "command_dirac_filter", + "command_eco_mode", + "command_lfc", + "command_containment_amount", + "command_loudness_management", + "command_bass_sync", + "command_dialog_enhancer", + "command_hdmi_output", + "command_hdmi_audio_decode", + "command_quick_select_mode", + "command_quick_select_memory", + "command_auto_standby", + "command_sleep", + "command_center_spread", + "command_video_processing_mode", + "command_room_size", + "command_status", + "command_system_reset", + "command_network_restart", + "command_trigger", + "command_speaker_preset", + "command_bluetooth_transmitter", + "command_dialog_control", + "command_speaker_virtualizer", + "command_effect_speaker_selection", + "command_drc", + "command_delay_time", + "command_audio_restorer", + "command_remote_control_lock", + "command_panel_lock", + "command_panel_and_volume_lock", + "command_graphic_eq", + "command_headphone_eq", ], ) @@ -363,6 +478,7 @@ COMMAND_NETAUDIO_POST_URL = "/NetAudio/index.put.asp" COMMAND_PAUSE = "/goform/formiPhoneAppDirect.xml?NS9B" COMMAND_PLAY = "/goform/formiPhoneAppDirect.xml?NS9A" +COMMAND_ECO_MODE = "/goform/formiPhoneAppDirect.xml?ECO{mode}" # Main Zone URLs @@ -379,6 +495,14 @@ COMMAND_MUTE_OFF_URL = "/goform/formiPhoneAppMute.xml?1+MuteOff" COMMAND_SEL_SM_URL = "/goform/formiPhoneAppDirect.xml?MS" COMMAND_SET_ZST_URL = "/goform/formiPhoneAppDirect.xml?MN" +COMMAND_NEURAL_X_ON_OFF = "/goform/formiPhoneAppDirect.xml?PSNEURAL%20{mode}" +COMMAND_IMAX_AUTO_OFF = "/goform/formiPhoneAppDirect.xml?PSIMAX%20{mode}" +COMMAND_IMAX_AUDIO_SETTINGS = "/goform/formiPhoneAppDirect.xml?PSIMAXAUD%20{mode}" +COMMAND_IMAX_HPF = "/goform/formiPhoneAppDirect.xml?PSIMAXHPF%20{frequency}" +COMMAND_IMAX_LPF = "/goform/formiPhoneAppDirect.xml?PSIMAXLPF%20{frequency}" +COMMAND_IMAX_SUBWOOFER_MODE = "/goform/formiPhoneAppDirect.xml?PSIMAXSWM%20{mode}" +COMMAND_IMAX_SUBWOOFER_OUTPUT = "/goform/formiPhoneAppDirect.xml?PSIMAXSWO%20{mode}" +COMMAND_CINEMA_EQ = "/goform/formiPhoneAppDirect.xml?PSCINEMA%20EQ.{mode}" COMMAND_CURSOR_UP = "/goform/formiPhoneAppDirect.xml?MNCUP" COMMAND_CURSOR_DOWN = "/goform/formiPhoneAppDirect.xml?MNCDN" COMMAND_CURSOR_LEFT = "/goform/formiPhoneAppDirect.xml?MNCLT" @@ -390,6 +514,56 @@ COMMAND_SETUP_OPEN = "/goform/formiPhoneAppDirect.xml?MNMEN%20ON" COMMAND_SETUP_CLOSE = "/goform/formiPhoneAppDirect.xml?MNMEN%20OFF" COMMAND_SETUP_QUERY = "/goform/formiPhoneAppDirect.xml?MNMEN?" +COMMAND_CHANNEL_LEVEL_ADJUST = "/goform/formiPhoneAppDirect.xml?MNCHL" +COMMAND_DIMMER_TOGGLE = "/goform/formiPhoneAppDirect.xml?DIM%20SEL" +COMMAND_DIMMER_SET = "/goform/formiPhoneAppDirect.xml?DIM%20{mode}" +COMMAND_CHANNEL_VOLUME = "/goform/formiPhoneAppDirect.xml?CV{channel}%20{value}" +COMMAND_CHANNEL_VOLUMES_RESET = "/goform/formiPhoneAppDirect.xml?CVZRL" +COMMAND_SUBWOOFER_ON_OFF = "/goform/formiPhoneAppDirect.xml?PSSWR%20{mode}" +COMMAND_SUBWOOFER_LEVEL = "/goform/formiPhoneAppDirect.xml?PSSWL{number}%20{mode}" +COMMAND_LFE = "/goform/formiPhoneAppDirect.xml?PSLFE%20{mode}" +COMMAND_TACTILE_TRANSDUCER = "/goform/formiPhoneAppDirect.xml?SSTTR%20{mode}" +COMMAND_TACTILE_TRANSDUCER_LEVEL = "/goform/formiPhoneAppDirect.xml?SSTTRLEV%20{mode}" +COMMAND_TACTILE_TRANSDUCER_LPF = ( + "/goform/formiPhoneAppDirect.xml?SSTTRLPF%20{frequency}" +) +COMMAND_DELAY_UP = "/goform/formiPhoneAppDirect.xml?PSDELAY%20UP" +COMMAND_DELAY_DOWN = "/goform/formiPhoneAppDirect.xml?PSDELAY%20DOWN" +COMMAND_AUROMATIC_3D_PRESET = "/goform/formiPhoneAppDirect.xml?PSAUROPR%20{preset}" +COMMAND_AUROMATIC_3D_STRENGTH = "/goform/formiPhoneAppDirect.xml?PSAUROST%20{value}" +COMMAND_AURO_3D_MODE = "/goform/formiPhoneAppDirect.xml?PSAUROMODEM%20{mode}" +COMMAND_DIRAC_FILTER = "/goform/formiPhoneAppDirect.xml?PSDIRAC%20{filter}" +COMMAND_LFC = "/goform/formiPhoneAppDirect.xml?PSLFC%20{mode}" +COMMAND_CONTAMINATION_AMOUNT = "/goform/formiPhoneAppDirect.xml?PSCNTAMT%20{value}" +COMMAND_LOUDNESS_MANAGEMENT = "/goform/formiPhoneAppDirect.xml?PSLOM%20{mode}" +COMMAND_BASS_SYNC = "/goform/formiPhoneAppDirect.xml?PSBSC%20{mode}" +COMMAND_DIALOG_ENHANCER = "/goform/formiPhoneAppDirect.xml?PSDEH%20{level}" +COMMAND_HDMI_OUTPUT = "/goform/formiPhoneAppDirect.xml?VSMONI{output}" +COMMAND_HDMI_AUDIO_DECODE = "/goform/formiPhoneAppDirect.xml?VSAUDIO%20{mode}" +COMMAND_QUICK_SELECT_MODE = "/goform/formiPhoneAppDirect.xml?MSQUICK{number}" +COMMAND_QUICK_SELECT_MEMORY = "/goform/formiPhoneAppDirect.xml?MSQUICK{number}" +COMMAND_AUTO_STANDBY = "/goform/formiPhoneAppDirect.xml?STBY{mode}" +COMMAND_SLEEP = "/goform/formiPhoneAppDirect.xml?SLP{value}" +COMMAND_CENTER_SPREAD = "/goform/formiPhoneAppDirect.xml?PSCES%20{mode}" +COMMAND_VIDEO_PROCESSING_MODE = "/goform/formiPhoneAppDirect.xml?VSVPM{mode}" +COMMAND_ROOM_SIZE = "/goform/formiPhoneAppDirect.xml?PSRSZ%20{size}" +COMMAND_STATUS = "/goform/formiPhoneAppDirect.xml?RCSHP0230030" +COMMAND_SYSTEM_RESET = "/goform/formiPhoneAppDirect.xml?SYRST" +COMMAND_NETWORK_RESTART = "/goform/formiPhoneAppDirect.xml?NSRBT" +COMMAND_TRIGGER = "/goform/formiPhoneAppDirect.xml?TR{number}%20{mode}" +COMMAND_SPEAKER_PRESET = "/goform/formiPhoneAppDirect.xml?SPPR%20{number}" +COMMAND_BLUETOOTH_TRANSMITTER = "/goform/formiPhoneAppDirect.xml?BTTX%20{mode}" +COMMAND_DIALOG_CONTROL = "/goform/formiPhoneAppDirect.xml?PSDIC%20{value}" +COMMAND_SPEAKER_VIRTUALIZER = "/goform/formiPhoneAppDirect.xml?PSSPV%20{mode}" +COMMAND_EFFECT_SPEAKER_SELECTION = "/goform/formiPhoneAppDirect.xml?PSSP:{mode}" +COMMAND_DRC = "/goform/formiPhoneAppDirect.xml?PSDRC%20{mode}" +COMMAND_DELAY_TIME = "/goform/formiPhoneAppDirect.xml?PSDEL%20{value}" +COMMAND_AUDIO_RESTORER = "/goform/formiPhoneAppDirect.xml?PSRSTR%20{mode}" +COMMAND_REMOTE_CONTROL_LOCK = "/goform/formiPhoneAppDirect.xml?SYREMOTE%20LOCK%20{mode}" +COMMAND_PANEL_LOCK = "/goform/formiPhoneAppDirect.xml?SYPANEL%20LOCK%20{mode}" +COMMAND_PANEL_AND_VOLUME_LOCK = "/goform/formiPhoneAppDirect.xml?SYPANEL+V%20LOCK%20ON" +COMMAND_GRAPHIC_EQ = "/goform/formiPhoneAppDirect.xml?PSGEQ%20{mode}" +COMMAND_HEADPHONE_EQ = "/goform/formiPhoneAppDirect.xml?PSHEQ%20{mode}" # Zone 2 URLs STATUS_Z2_URL = "/goform/formZone2_Zone2XmlStatus.xml" @@ -434,6 +608,14 @@ command_mute_on=COMMAND_MUTE_ON_URL, command_mute_off=COMMAND_MUTE_OFF_URL, command_sel_sound_mode=COMMAND_SEL_SM_URL, + command_neural_x_on_off=COMMAND_NEURAL_X_ON_OFF, + command_imax_auto_off=COMMAND_IMAX_AUTO_OFF, + command_imax_audio_settings=COMMAND_IMAX_AUTO_OFF, + command_imax_hpf=COMMAND_IMAX_HPF, + command_imax_lpf=COMMAND_IMAX_LPF, + command_imax_subwoofer_mode=COMMAND_IMAX_SUBWOOFER_MODE, + command_imax_subwoofer_output=COMMAND_IMAX_SUBWOOFER_OUTPUT, + command_cinema_eq=COMMAND_CINEMA_EQ, command_netaudio_post=COMMAND_NETAUDIO_POST_URL, command_set_all_zone_stereo=COMMAND_SET_ZST_URL, command_pause=COMMAND_PAUSE, @@ -449,6 +631,55 @@ command_setup_open=COMMAND_SETUP_OPEN, command_setup_close=COMMAND_SETUP_CLOSE, command_setup_query=COMMAND_SETUP_QUERY, + command_channel_level_adjust=COMMAND_CHANNEL_LEVEL_ADJUST, + command_dimmer_toggle=COMMAND_DIMMER_TOGGLE, + command_dimmer_set=COMMAND_DIMMER_SET, + command_channel_volume=COMMAND_CHANNEL_VOLUME, + command_channel_volumes_reset=COMMAND_CHANNEL_VOLUMES_RESET, + command_subwoofer_on_off=COMMAND_SUBWOOFER_ON_OFF, + command_subwoofer_level=COMMAND_SUBWOOFER_LEVEL, + command_lfe=COMMAND_LFE, + command_tactile_transducer=COMMAND_TACTILE_TRANSDUCER, + command_tactile_transducer_level=COMMAND_TACTILE_TRANSDUCER_LEVEL, + command_tactile_transducer_lpf=COMMAND_TACTILE_TRANSDUCER_LPF, + command_delay_up=COMMAND_DELAY_UP, + command_delay_down=COMMAND_DELAY_DOWN, + command_auromatic_3d_preset=COMMAND_AUROMATIC_3D_PRESET, + command_auromatic_3d_strength=COMMAND_AUROMATIC_3D_STRENGTH, + command_auro_3d_mode=COMMAND_AURO_3D_MODE, + command_dirac_filter=COMMAND_DIRAC_FILTER, + command_eco_mode=COMMAND_ECO_MODE, + command_lfc=COMMAND_LFC, + command_containment_amount=COMMAND_CONTAMINATION_AMOUNT, + command_loudness_management=COMMAND_LOUDNESS_MANAGEMENT, + command_bass_sync=COMMAND_BASS_SYNC, + command_dialog_enhancer=COMMAND_DIALOG_ENHANCER, + command_hdmi_output=COMMAND_HDMI_OUTPUT, + command_hdmi_audio_decode=COMMAND_HDMI_AUDIO_DECODE, + command_quick_select_mode=COMMAND_QUICK_SELECT_MODE, + command_quick_select_memory=COMMAND_QUICK_SELECT_MODE, + command_auto_standby=COMMAND_AUTO_STANDBY, + command_sleep=COMMAND_SLEEP, + command_center_spread=COMMAND_CENTER_SPREAD, + command_video_processing_mode=COMMAND_VIDEO_PROCESSING_MODE, + command_room_size=COMMAND_ROOM_SIZE, + command_status=COMMAND_STATUS, + command_system_reset=COMMAND_SYSTEM_RESET, + command_network_restart=COMMAND_NETWORK_RESTART, + command_trigger=COMMAND_TRIGGER, + command_speaker_preset=COMMAND_SPEAKER_PRESET, + command_bluetooth_transmitter=COMMAND_BLUETOOTH_TRANSMITTER, + command_dialog_control=COMMAND_DIALOG_CONTROL, + command_speaker_virtualizer=COMMAND_SPEAKER_VIRTUALIZER, + command_effect_speaker_selection=COMMAND_EFFECT_SPEAKER_SELECTION, + command_drc=COMMAND_DRC, + command_delay_time=COMMAND_DELAY_TIME, + command_audio_restorer=COMMAND_AUDIO_RESTORER, + command_remote_control_lock=COMMAND_REMOTE_CONTROL_LOCK, + command_panel_lock=COMMAND_PANEL_LOCK, + command_panel_and_volume_lock=COMMAND_PANEL_AND_VOLUME_LOCK, + command_graphic_eq=COMMAND_GRAPHIC_EQ, + command_headphone_eq=COMMAND_HEADPHONE_EQ, ) ZONE2_URLS = ReceiverURLs( @@ -470,6 +701,14 @@ command_mute_on=COMMAND_MUTE_ON_Z2_URL, command_mute_off=COMMAND_MUTE_OFF_Z2_URL, command_sel_sound_mode=COMMAND_SEL_SM_URL, + command_neural_x_on_off=COMMAND_NEURAL_X_ON_OFF, + command_imax_auto_off=COMMAND_IMAX_AUTO_OFF, + command_imax_audio_settings=COMMAND_IMAX_AUDIO_SETTINGS, + command_imax_hpf=COMMAND_IMAX_HPF, + command_imax_lpf=COMMAND_IMAX_LPF, + command_imax_subwoofer_mode=COMMAND_IMAX_SUBWOOFER_MODE, + command_imax_subwoofer_output=COMMAND_IMAX_SUBWOOFER_OUTPUT, + command_cinema_eq=COMMAND_CINEMA_EQ, command_netaudio_post=COMMAND_NETAUDIO_POST_URL, command_set_all_zone_stereo=COMMAND_SET_ZST_URL, command_pause=COMMAND_PAUSE, @@ -485,6 +724,55 @@ command_setup_open=COMMAND_SETUP_OPEN, command_setup_close=COMMAND_SETUP_CLOSE, command_setup_query=COMMAND_SETUP_QUERY, + command_channel_level_adjust=COMMAND_CHANNEL_LEVEL_ADJUST, + command_dimmer_toggle=COMMAND_DIMMER_TOGGLE, + command_dimmer_set=COMMAND_DIMMER_SET, + command_channel_volume=COMMAND_CHANNEL_VOLUME, + command_channel_volumes_reset=COMMAND_CHANNEL_VOLUMES_RESET, + command_subwoofer_on_off=COMMAND_SUBWOOFER_ON_OFF, + command_subwoofer_level=COMMAND_SUBWOOFER_LEVEL, + command_lfe=COMMAND_LFE, + command_tactile_transducer=COMMAND_TACTILE_TRANSDUCER, + command_tactile_transducer_level=COMMAND_TACTILE_TRANSDUCER_LEVEL, + command_tactile_transducer_lpf=COMMAND_TACTILE_TRANSDUCER_LPF, + command_delay_up=COMMAND_DELAY_UP, + command_delay_down=COMMAND_DELAY_DOWN, + command_auromatic_3d_preset=COMMAND_AUROMATIC_3D_PRESET, + command_auromatic_3d_strength=COMMAND_AUROMATIC_3D_STRENGTH, + command_auro_3d_mode=COMMAND_AURO_3D_MODE, + command_dirac_filter=COMMAND_DIRAC_FILTER, + command_eco_mode=COMMAND_ECO_MODE, + command_lfc=COMMAND_LFC, + command_containment_amount=COMMAND_CONTAMINATION_AMOUNT, + command_loudness_management=COMMAND_LOUDNESS_MANAGEMENT, + command_bass_sync=COMMAND_BASS_SYNC, + command_dialog_enhancer=COMMAND_DIALOG_ENHANCER, + command_hdmi_output=COMMAND_HDMI_OUTPUT, + command_hdmi_audio_decode=COMMAND_HDMI_AUDIO_DECODE, + command_quick_select_mode=COMMAND_QUICK_SELECT_MODE, + command_quick_select_memory=COMMAND_QUICK_SELECT_MEMORY, + command_auto_standby=COMMAND_AUTO_STANDBY, + command_sleep=COMMAND_SLEEP, + command_center_spread=COMMAND_CENTER_SPREAD, + command_video_processing_mode=COMMAND_VIDEO_PROCESSING_MODE, + command_room_size=COMMAND_ROOM_SIZE, + command_status=COMMAND_STATUS, + command_system_reset=COMMAND_SYSTEM_RESET, + command_network_restart=COMMAND_NETWORK_RESTART, + command_trigger=COMMAND_TRIGGER, + command_speaker_preset=COMMAND_SPEAKER_PRESET, + command_bluetooth_transmitter=COMMAND_BLUETOOTH_TRANSMITTER, + command_dialog_control=COMMAND_DIALOG_CONTROL, + command_speaker_virtualizer=COMMAND_SPEAKER_VIRTUALIZER, + command_effect_speaker_selection=COMMAND_EFFECT_SPEAKER_SELECTION, + command_drc=COMMAND_DRC, + command_delay_time=COMMAND_DELAY_TIME, + command_audio_restorer=COMMAND_AUDIO_RESTORER, + command_remote_control_lock=COMMAND_REMOTE_CONTROL_LOCK, + command_panel_lock=COMMAND_PANEL_LOCK, + command_panel_and_volume_lock=COMMAND_PANEL_AND_VOLUME_LOCK, + command_graphic_eq=COMMAND_GRAPHIC_EQ, + command_headphone_eq=COMMAND_HEADPHONE_EQ, ) ZONE3_URLS = ReceiverURLs( @@ -506,6 +794,14 @@ command_mute_on=COMMAND_MUTE_ON_Z3_URL, command_mute_off=COMMAND_MUTE_OFF_Z3_URL, command_sel_sound_mode=COMMAND_SEL_SM_URL, + command_neural_x_on_off=COMMAND_NEURAL_X_ON_OFF, + command_imax_auto_off=COMMAND_IMAX_AUTO_OFF, + command_imax_audio_settings=COMMAND_IMAX_AUDIO_SETTINGS, + command_imax_hpf=COMMAND_IMAX_HPF, + command_imax_lpf=COMMAND_IMAX_LPF, + command_imax_subwoofer_mode=COMMAND_IMAX_SUBWOOFER_MODE, + command_imax_subwoofer_output=COMMAND_IMAX_SUBWOOFER_OUTPUT, + command_cinema_eq=COMMAND_CINEMA_EQ, command_netaudio_post=COMMAND_NETAUDIO_POST_URL, command_set_all_zone_stereo=COMMAND_SET_ZST_URL, command_pause=COMMAND_PAUSE, @@ -521,11 +817,61 @@ command_setup_open=COMMAND_SETUP_OPEN, command_setup_close=COMMAND_SETUP_CLOSE, command_setup_query=COMMAND_SETUP_QUERY, + command_channel_level_adjust=COMMAND_CHANNEL_LEVEL_ADJUST, + command_dimmer_toggle=COMMAND_DIMMER_TOGGLE, + command_dimmer_set=COMMAND_DIMMER_SET, + command_channel_volume=COMMAND_CHANNEL_VOLUME, + command_channel_volumes_reset=COMMAND_CHANNEL_VOLUMES_RESET, + command_subwoofer_on_off=COMMAND_SUBWOOFER_ON_OFF, + command_subwoofer_level=COMMAND_SUBWOOFER_LEVEL, + command_lfe=COMMAND_LFE, + command_tactile_transducer=COMMAND_TACTILE_TRANSDUCER, + command_tactile_transducer_level=COMMAND_TACTILE_TRANSDUCER_LEVEL, + command_tactile_transducer_lpf=COMMAND_TACTILE_TRANSDUCER_LPF, + command_delay_up=COMMAND_DELAY_UP, + command_delay_down=COMMAND_DELAY_DOWN, + command_auromatic_3d_preset=COMMAND_AUROMATIC_3D_PRESET, + command_auromatic_3d_strength=COMMAND_AUROMATIC_3D_STRENGTH, + command_auro_3d_mode=COMMAND_AURO_3D_MODE, + command_dirac_filter=COMMAND_DIRAC_FILTER, + command_eco_mode=COMMAND_ECO_MODE, + command_lfc=COMMAND_LFC, + command_containment_amount=COMMAND_CONTAMINATION_AMOUNT, + command_loudness_management=COMMAND_LOUDNESS_MANAGEMENT, + command_bass_sync=COMMAND_BASS_SYNC, + command_dialog_enhancer=COMMAND_DIALOG_ENHANCER, + command_hdmi_output=COMMAND_HDMI_OUTPUT, + command_hdmi_audio_decode=COMMAND_HDMI_AUDIO_DECODE, + command_quick_select_mode=COMMAND_QUICK_SELECT_MODE, + command_quick_select_memory=COMMAND_QUICK_SELECT_MEMORY, + command_auto_standby=COMMAND_AUTO_STANDBY, + command_sleep=COMMAND_SLEEP, + command_center_spread=COMMAND_CENTER_SPREAD, + command_video_processing_mode=COMMAND_VIDEO_PROCESSING_MODE, + command_room_size=COMMAND_ROOM_SIZE, + command_status=COMMAND_STATUS, + command_system_reset=COMMAND_SYSTEM_RESET, + command_network_restart=COMMAND_NETWORK_RESTART, + command_trigger=COMMAND_TRIGGER, + command_speaker_preset=COMMAND_SPEAKER_PRESET, + command_bluetooth_transmitter=COMMAND_BLUETOOTH_TRANSMITTER, + command_dialog_control=COMMAND_DIALOG_CONTROL, + command_speaker_virtualizer=COMMAND_SPEAKER_VIRTUALIZER, + command_effect_speaker_selection=COMMAND_EFFECT_SPEAKER_SELECTION, + command_drc=COMMAND_DRC, + command_delay_time=COMMAND_DELAY_TIME, + command_audio_restorer=COMMAND_AUDIO_RESTORER, + command_remote_control_lock=COMMAND_REMOTE_CONTROL_LOCK, + command_panel_lock=COMMAND_PANEL_LOCK, + command_panel_and_volume_lock=COMMAND_PANEL_AND_VOLUME_LOCK, + command_graphic_eq=COMMAND_GRAPHIC_EQ, + command_headphone_eq=COMMAND_HEADPHONE_EQ, ) # Telnet Events ALL_TELNET_EVENTS = "ALL" TELNET_EVENTS = { + "BT", "CV", "DC", "DIM", @@ -546,6 +892,7 @@ "SD", "SI", "SLP", + "SP", "SR", "SS", "STBY", @@ -589,6 +936,14 @@ command_mute_on="MUON", command_mute_off="MUOFF", command_sel_sound_mode="MS", + command_neural_x_on_off="PSNEURAL {mode}", + command_imax_auto_off="PSIMAX {mode}", + command_imax_audio_settings="PSIMAXAUD {mode}", + command_imax_hpf="PSIMAXHPF {frequency}", + command_imax_lpf="PSIMAXLPF {frequency}", + command_imax_subwoofer_mode="PSIMAXSWM {mode}", + command_imax_subwoofer_output="PSIMAXSWO {mode}", + command_cinema_eq="PSCINEMA EQ.{mode}", command_set_all_zone_stereo="MN", command_pause="NS9B", command_play="NS9A", @@ -610,6 +965,55 @@ command_setup_open="MNMEN ON", command_setup_close="MNMEN OFF", command_setup_query="MNMEN?", + command_channel_level_adjust="MNCHL", + command_dimmer_toggle="DIM SEL", + command_dimmer_set="DIM {mode}", + command_channel_volume="CV{channel} {value}", + command_channel_volumes_reset="CVZRL", + command_subwoofer_on_off="PSSWR {mode}", + command_subwoofer_level="PSSWL{number} {mode}", + command_lfe="PSLFE {mode}", + command_tactile_transducer="SSTTR {mode}", + command_tactile_transducer_level="SSTTRLEV {mode}", + command_tactile_transducer_lpf="SSTTRLPF {frequency}", + command_delay_up="PSDELAY UP", + command_delay_down="PSDELAY DOWN", + command_auromatic_3d_preset="PSAUROPR {preset}", + command_auromatic_3d_strength="PSAUROST {value}", + command_auro_3d_mode="PSAUROMODE {mode}", + command_dirac_filter="PSDIRAC {filter}", + command_eco_mode="ECO{mode}", + command_lfc="PSLFC {mode}", + command_containment_amount="PSCNTAMT {value}", + command_loudness_management="PSLOM {mode}", + command_bass_sync="PSBSC {mode}", + command_dialog_enhancer="PSDEH {level}", + command_hdmi_output="VSMONI{output}", + command_hdmi_audio_decode="VSAUDIO {mode}", + command_quick_select_mode="MSQUICK{number}", + command_quick_select_memory="MSQUICK{number} MEMORY", + command_auto_standby="STBY{mode}", + command_sleep="SLP{value}", + command_center_spread="PSCES {mode}", + command_video_processing_mode="VSVPM{mode}", + command_room_size="PSRSZ {size}", + command_status="RCSHP0230030", + command_system_reset="SYRST", + command_network_restart="NSRBT", + command_trigger="TR{number} {mode}", + command_speaker_preset="SPPR {number}", + command_bluetooth_transmitter="BTTX {mode}", + command_dialog_control="PSDIC {value}", + command_speaker_virtualizer="PSSPV {mode}", + command_effect_speaker_selection="PSSP:{mode}", + command_drc="PSDRC {mode}", + command_delay_time="PSDEL {value}", + command_audio_restorer="PSRSTR {mode}", + command_remote_control_lock="SYREMOTE LOCK {mode}", + command_panel_lock="SYPANEL LOCK {mode}", + command_panel_and_volume_lock="SYPANEL+V LOCK ON", + command_graphic_eq="PSGEQ {mode}", + command_headphone_eq="PSHEQ {mode}", ) ZONE2_TELNET_COMMANDS = TelnetCommands( @@ -623,6 +1027,14 @@ command_mute_on="Z2MUON", command_mute_off="Z2MUOFF", command_sel_sound_mode="MS", + command_neural_x_on_off="PSNEURAL {mode}", + command_imax_auto_off="PSIMAX {mode}", + command_imax_audio_settings="PSIMAXAUD {mode}", + command_imax_hpf="PSIMAXHPF {frequency}", + command_imax_lpf="PSIMAXLPF {frequency}", + command_imax_subwoofer_mode="PSIMAXSWM {mode}", + command_imax_subwoofer_output="PSIMAXSWO {mode}", + command_cinema_eq="PSCINEMA EQ.{mode}", command_set_all_zone_stereo="MN", command_pause="NS9B", command_play="NS9A", @@ -644,6 +1056,55 @@ command_setup_open="MNMEN ON", command_setup_close="MNMEN OFF", command_setup_query="MNMEN?", + command_channel_level_adjust="MNCHL", + command_dimmer_toggle="DIM SEL", + command_dimmer_set="DIM {mode}", + command_channel_volume="CV{channel} {value}", + command_channel_volumes_reset="CVZRL", + command_subwoofer_on_off="PSSWR {mode}", + command_subwoofer_level="PSSWL{number} {mode}", + command_lfe="PSLFE {mode}", + command_tactile_transducer="SSTTR {mode}", + command_tactile_transducer_level="SSTTRLEV {mode}", + command_tactile_transducer_lpf="SSTTRLPF {frequency}", + command_delay_up="PSDELAY UP", + command_delay_down="PSDELAY DOWN", + command_auromatic_3d_preset="PSAUROPR {preset}", + command_auromatic_3d_strength="PSAUROST {value}", + command_auro_3d_mode="PSAUROMODE {mode}", + command_dirac_filter="PSDIRAC {filter}", + command_eco_mode="ECO{mode}", + command_lfc="PSLFC {mode}", + command_containment_amount="PSCNTAMT {value}", + command_loudness_management="PSLOM {mode}", + command_bass_sync="PSBSC {mode}", + command_dialog_enhancer="PSDEH {level}", + command_hdmi_output="VSMONI{output}", + command_hdmi_audio_decode="VSAUDIO {mode}", + command_quick_select_mode="MSQUICK{number}", + command_quick_select_memory="MSQUICK{number} MEMORY", + command_auto_standby="STBY{mode}", + command_sleep="SLP{value}", + command_center_spread="PSCES {mode}", + command_video_processing_mode="VSVPM{mode}", + command_room_size="PSRSZ {size}", + command_status="RCSHP0230030", + command_system_reset="SYRST", + command_network_restart="NSRBT", + command_trigger="TR{number} {mode}", + command_speaker_preset="SPPR {number}", + command_bluetooth_transmitter="BTTX {mode}", + command_dialog_control="PSDIC {value}", + command_speaker_virtualizer="PSSPV {mode}", + command_effect_speaker_selection="PSSP:{mode}", + command_drc="PSDRC {mode}", + command_delay_time="PSDEL {value}", + command_audio_restorer="PSRSTR {mode}", + command_remote_control_lock="SYREMOTE LOCK {mode}", + command_panel_lock="SYPANEL LOCK {mode}", + command_panel_and_volume_lock="SYPANEL+V LOCK ON", + command_graphic_eq="PSGEQ {mode}", + command_headphone_eq="PSHEQ {mode}", ) ZONE3_TELNET_COMMANDS = TelnetCommands( @@ -657,6 +1118,14 @@ command_mute_on="Z3MUON", command_mute_off="Z3MUOFF", command_sel_sound_mode="MS", + command_neural_x_on_off="PSNEURAL {mode}", + command_imax_auto_off="PSIMAX {mode}", + command_imax_audio_settings="PSIMAXAUD {mode}", + command_imax_hpf="PSIMAXHPF {frequency}", + command_imax_lpf="PSIMAXLPF {frequency}", + command_imax_subwoofer_mode="PSIMAXSWM {mode}", + command_imax_subwoofer_output="PSIMAXSWO {mode}", + command_cinema_eq="PSCINEMA EQ.{mode}", command_set_all_zone_stereo="MN", command_pause="NS9B", command_play="NS9A", @@ -678,6 +1147,55 @@ command_setup_open="MNMEN ON", command_setup_close="MNMEN OFF", command_setup_query="MNMEN?", + command_channel_level_adjust="MNCHL", + command_dimmer_toggle="DIM SEL", + command_dimmer_set="DIM {mode}", + command_channel_volume="CV{channel} {value}", + command_channel_volumes_reset="CVZRL", + command_subwoofer_on_off="PSSWR {mode}", + command_subwoofer_level="PSSWL{number} {mode}", + command_lfe="PSLFE {mode}", + command_tactile_transducer="SSTTR {mode}", + command_tactile_transducer_level="SSTTRLEV {mode}", + command_tactile_transducer_lpf="SSTTRLPF {frequency}", + command_delay_up="PSDELAY UP", + command_delay_down="PSDELAY DOWN", + command_auromatic_3d_preset="PSAUROPR {preset}", + command_auromatic_3d_strength="PSAUROST {value}", + command_auro_3d_mode="PSAUROMODE {mode}", + command_dirac_filter="PSDIRAC {filter}", + command_eco_mode="ECO{mode}", + command_lfc="PSLFC {mode}", + command_containment_amount="PSCNTAMT {value}", + command_loudness_management="PSLOM {mode}", + command_bass_sync="PSBSC {mode}", + command_dialog_enhancer="PSDEH {level}", + command_hdmi_output="VSMONI{output}", + command_hdmi_audio_decode="VSAUDIO {mode}", + command_quick_select_mode="MSQUICK{number}", + command_quick_select_memory="MSQUICK{number} MEMORY", + command_auto_standby="STBY{mode}", + command_sleep="SLP{value}", + command_center_spread="PSCES {mode}", + command_video_processing_mode="VSVPM{mode}", + command_room_size="PSRSZ {size}", + command_status="RCSHP0230030", + command_system_reset="SYRST", + command_network_restart="NSRBT", + command_trigger="TR{number} {mode}", + command_speaker_preset="SPPR {number}", + command_bluetooth_transmitter="BTTX {mode}", + command_dialog_control="PSDIC {value}", + command_speaker_virtualizer="PSSPV {mode}", + command_effect_speaker_selection="PSSP:{mode}", + command_drc="PSDRC {mode}", + command_delay_time="PSDEL {value}", + command_audio_restorer="PSRSTR {mode}", + command_remote_control_lock="SYREMOTE LOCK {mode}", + command_panel_lock="SYPANEL LOCK {mode}", + command_panel_and_volume_lock="SYPANEL+V LOCK ON", + command_graphic_eq="PSGEQ {mode}", + command_headphone_eq="PSHEQ {mode}", ) # States @@ -692,6 +1210,11 @@ SETTINGS_MENU_ON = "ON" SETTINGS_MENU_OFF = "OFF" SETTINGS_MENU_STATES = {SETTINGS_MENU_ON, SETTINGS_MENU_OFF} +DIMER_BRIGHT = "BRI" +DIMER_DIM = "DIM" +DIMER_DARK = "DAR" +DIMER_OFF = "OFF" +DIMMER_STATES = {DIMER_BRIGHT, DIMER_DIM, DIMER_DARK, DIMER_OFF} # Zones ALL_ZONES = "All" @@ -749,3 +1272,318 @@ DYNAMIC_VOLUME_MAP_LABELS_TELNET = { value: key for key, value in DYNAMIC_VOLUME_MAP_TELNET.items() } + +AudioRestorers = Literal["Off", "Low", "Medium", "High"] +"""Audio Restorer settings.""" + +AUDIO_RESTORER_MAP = { + "Off": "OFF", + "Low": "LOW", + "Medium": "MED", + "High": "HI", +} +AUDIO_RESTORER_MAP_LABELS = {value: key for key, value in AUDIO_RESTORER_MAP.items()} + +AuroMatic3DPresets = Literal[ + "Small", + "Medium", + "Large", + "Speech", + "Movie", +] +"""Auro-Matic 3D Presets.""" + +AURO_MATIC_3D_PRESET_MAP = { + "Small": "SMA", + "Medium": "MED", + "Large": "LAR", + "Speech": "SPE", + "Movie": "MOV", +} +AURO_MATIC_3D_PRESET_MAP_LABELS = { + value: key for key, value in AURO_MATIC_3D_PRESET_MAP.items() +} + +Auro3DModes = Literal["Direct", "Channel Expansion"] +"""Auro 3D Modes.""" + +AURO_3D_MODE_MAP = {"Direct": "DRCT", "Channel Expansion": "EXP"} +AURO_3D_MODE_MAP_MAP_LABELS = {value: key for key, value in AURO_3D_MODE_MAP.items()} + +AutoStandbys = Literal["OFF", "15M", "30M", "60M"] + +BluetoothOutputModes = Literal["Bluetooth + Speakers", "Bluetooth Only"] +"""Bluetooth Output Modes.""" + +BLUETOOTH_OUTPUT_MODES_MAP = { + "Bluetooth + Speakers": "SP", + "Bluetooth Only": "BT", +} +BLUETOOTH_OUTPUT_MAP_LABELS = { + value: key for key, value in BLUETOOTH_OUTPUT_MODES_MAP.items() +} + +DIMMER_MODE_MAP = { + "Off": DIMER_OFF, + "Dark": DIMER_DARK, + "Dim": DIMER_DIM, + "Bright": DIMER_BRIGHT, +} +DIMMER_MODE_MAP_LABELS = {value: key for key, value in DIMMER_MODE_MAP.items()} + +DimmerModes = Literal["Off", "Dark", "Dim", "Bright"] +"""Dimmer Modes.""" + +DIRAC_FILTER_MAP = {"Off": "OFF", "Slot 1": "1", "Slot 2": "2", "Slot 3": "3"} +DIRAC_FILTER_MAP_LABELS = {value: key for key, value in DIRAC_FILTER_MAP.items()} + +DiracFilters = Literal["Slot 1", "Slot 2", "Slot 3", "Off"] +"""Dirac Filters.""" + +ECO_MODE_MAP = { + "On": "ON", + "Auto": "AUTO", + "Off": "OFF", +} +ECO_MODE_MAP_LABELS = {value: key for key, value in ECO_MODE_MAP.items()} + +EcoModes = Literal["On", "Auto", "Off"] +"""Eco Modes.""" + +EffectSpeakers = Literal["Floor", "Height + Floor"] +"""Effect Speakers.""" + +EFFECT_SPEAKER_SELECTION_MAP = { + "Floor": "FL", + "Height + Floor": "HF", +} +EFFECT_SPEAKER_SELECTION_MAP_LABELS = { + value: key for key, value in EFFECT_SPEAKER_SELECTION_MAP.items() +} + +DRCs = Literal["AUTO", "LOW", "MID", "HI", "OFF"] +"""Dynamic Range Control (DRC) Settings.""" + +HDMI_OUTPUT_MAP = { + "Auto": "AUTO", + "HDMI1": "1", + "HDMI2": "2", +} +HDMI_OUTPUT_MAP_LABELS = { + "MONIAUTO": "Auto", + "MONI1": "HDMI1", + "MONI2": "HDMI2", +} + +HDMIOutputs = Literal["Auto", "HDMI1", "HDMI2"] +"""HDMI Output Modes.""" + +HDMIAudioDecodes = Literal["AMP", "TV"] + +Subwoofers = Literal["Subwoofer", "Subwoofer 2", "Subwoofer 3", "Subwoofer 4"] +"""Subwoofers.""" + +SUBWOOFERS_MAP = { + "Subwoofer": "", + "Subwoofer 2": "2", + "Subwoofer 3": "3", + "Subwoofer 4": "4", +} +"""Subwoofers.""" + +SUBWOOFERS_MAP_LABELS = { + "SWL": "Subwoofer", + "SWL2": "Subwoofer 2", + "SWL3": "Subwoofer 3", + "SWL4": "Subwoofer 4", +} + +CHANNEL_MAP = { + "Front Left": "FL", + "Front Right": "FR", + "Center": "C", + "Subwoofer": "SW", + "Subwoofer 2": "SW2", + "Subwoofer 3": "SW3", + "Subwoofer 4": "SW4", + "Surround Left": "SL", + "Surround Right": "SR", + "Surround Back Left": "SBL", + "Surround Back Right": "SBR", + "Front Height Left": "FHL", + "Front Height Right": "FHR", + "Front Wide Left": "FWL", + "Front Wide Right": "FWR", + "Top Front Left": "TFL", + "Top Front Right": "TFR", + "Top Middle Left": "TML", + "Top Middle Right": "TMR", + "Top Rear Left": "TRL", + "Top Rear Right": "TRR", + "Rear Height Left": "RHL", + "Rear Height Right": "RHR", + "Front Dolby Left": "FDL", + "Front Dolby Right": "FDR", + "Surround Dolby Left": "SDL", + "Surround Dolby Right": "SDR", + "Back Dolby Left": "BDL", + "Back Dolby Right": "BDR", + "Surround Height Left": "SHL", + "Surround Height Right": "SHR", + "Top Surround": "TS", + "Center Height": "CH", +} +CHANNEL_MAP_LABELS = {value: key for key, value in CHANNEL_MAP.items()} + +Channels = Literal[ + "Front Left", + "Front Right", + "Center", + "Subwoofer", + "Subwoofer 2", + "Subwoofer 3", + "Subwoofer 4", + "Surround Left", + "Surround Right", + "Surround Back Left", + "Surround Back Right", + "Front Height Left", + "Front Height Right", + "Front Wide Left", + "Front Wide Right", + "Top Front Left", + "Top Front Right", + "Top Middle Left", + "Top Middle Right", + "Top Rear Left", + "Top Rear Right", + "Rear Height Left", + "Rear Height Right", + "Front Dolby Left", + "Front Dolby Right", + "Surround Dolby Left", + "Surround Dolby Right", + "Back Dolby Left", + "Back Dolby Right", + "Surround Height Left", + "Surround Height Right", + "Top Surround", + "Center Height", +] +"""Receiver Channels.""" + +CHANNEL_VOLUME_MAP = { + "38": -12.0, + "385": -11.5, + "39": -11.0, + "395": -10.5, + "40": -10.0, + "405": -9.5, + "41": -9.0, + "415": -8.5, + "42": -8.0, + "425": -7.5, + "43": -7.0, + "435": -6.5, + "44": -6.0, + "445": -5.5, + "45": -5.0, + "455": -4.5, + "46": -4.0, + "465": -3.5, + "47": -3.0, + "475": -2.5, + "48": -2.0, + "485": -1.5, + "49": -1.0, + "495": -0.5, + "50": 0.0, + "505": 0.5, + "51": 1.0, + "515": 1.5, + "52": 2.0, + "525": 2.5, + "53": 3.0, + "535": 3.5, + "54": 4.0, + "545": 4.5, + "55": 5.0, + "555": 5.5, + "56": 6.0, + "565": 6.5, + "57": 7.0, + "575": 7.5, + "58": 8.0, + "585": 8.5, + "59": 9.0, + "595": 9.5, + "60": 10.0, + "605": 10.5, + "61": 11.0, + "615": 11.5, + "62": 12.0, +} +CHANNEL_VOLUME_MAP_LABELS = {value: key for key, value in CHANNEL_VOLUME_MAP.items()} + +DialogEnhancerLevels = Literal["Off", "Low", "Medium", "High"] +"""Dialog Enhancer Levels.""" + +DIALOG_ENHANCER_LEVEL_MAP = { + "Off": "OFF", + "Low": "LOW", + "Medium": "MED", + "High": "HIGH", +} + +DIALOG_ENHANCER_LEVEL_MAP_LABELS = { + value: key for key, value in DIALOG_ENHANCER_LEVEL_MAP.items() +} + +TransducerLPFs = Literal[ + "40 Hz", + "60 Hz", + "80 Hz", + "90 Hz", + "100 Hz", + "110 Hz", + "120 Hz", + "150 Hz", + "180 Hz", + "200 Hz", + "250 Hz", +] +"""Tactile Transducer Low Pass Frequencies.""" + +IMAXHPFs = Literal[ + "40", "60", "80", "90", "100", "110", "120", "150", "180", "200", "250" +] +"""IMAX High Pass Frequencies.""" + +IMAXLPFs = Literal["80", "90", "100", "110", "120", "150", "180", "200", "250"] +"""IMAX Low Pass Frequencies.""" + +PanelLocks = Literal["Panel", "Panel + Master Volume"] +"""Panel Lock Modes.""" + +RoomSizes = Literal[ + "S", + "MS", + "M", + "ML", + "L", +] +"""Room Sizes.""" + +VideoProcessingModes = Literal["Auto", "Game", "Movie", "Bypass"] +"""Video Processing Modes.""" + +VIDEO_PROCESSING_MODES_MAP = { + "Auto": "AUTO", + "Game": "GAME", + "Movie": "MOVI", + "Bypass": "BYP", +} + +VIDEO_PROCESSING_MODES_MAP_LABELS = { + value: key for key, value in VIDEO_PROCESSING_MODES_MAP.items() +} diff --git a/denonavr/denonavr.py b/denonavr/denonavr.py index f15ea30..53f797e 100644 --- a/denonavr/denonavr.py +++ b/denonavr/denonavr.py @@ -10,13 +10,29 @@ import asyncio import logging import time -from typing import Awaitable, Callable, Dict, List, Optional +from typing import Awaitable, Callable, Dict, List, Literal, Optional, Union import attr import httpx from .audyssey import DenonAVRAudyssey, audyssey_factory -from .const import DENON_ATTR_SETATTR, MAIN_ZONE, VALID_ZONES +from .const import ( + DENON_ATTR_SETATTR, + MAIN_ZONE, + VALID_ZONES, + AudioRestorers, + AutoStandbys, + BluetoothOutputModes, + DimmerModes, + EcoModes, + HDMIAudioDecodes, + HDMIOutputs, + PanelLocks, + RoomSizes, + TransducerLPFs, + VideoProcessingModes, +) +from .dirac import DenonAVRDirac, dirac_factory from .exceptions import AvrCommandError from .foundation import DenonAVRFoundation, set_api_host, set_api_timeout from .input import DenonAVRInput, input_factory @@ -83,6 +99,11 @@ class DenonAVR(DenonAVRFoundation): default=attr.Factory(audyssey_factory, takes_self=True), init=False, ) + dirac: DenonAVRDirac = attr.ib( + validator=attr.validators.instance_of(DenonAVRDirac), + default=attr.Factory(dirac_factory, takes_self=True), + init=False, + ) input: DenonAVRInput = attr.ib( validator=attr.validators.instance_of(DenonAVRInput), default=attr.Factory(input_factory, takes_self=True), @@ -149,6 +170,7 @@ async def async_setup(self) -> None: await self.tonecontrol.async_setup() self.vol.setup() self.audyssey.setup() + self.dirac.setup() for zone_name, zone_item in self._zones.items(): if zone_name != self.zone: @@ -279,11 +301,11 @@ def power(self) -> Optional[str]: return self._device.power @property - def settings_menu(self) -> Optional[str]: + def settings_menu(self) -> Optional[bool]: """ - Return the settings menu state of the device. Only available if using Telnet. + Return the settings menu state of the device. - Possible values are: "ON" and "OFF" + Only available if using Telnet. """ return self._device.settings_menu @@ -514,6 +536,224 @@ def multi_eq_setting_list(self) -> List[str]: """Return a list of available MultiEQ settings.""" return self.audyssey.multi_eq_setting_list + @property + def dimmer(self) -> Optional[str]: + """ + Returns the dimmer state of the device. + + Only available if using Telnet. + + Possible values are: "Off", "Dark", "Dim" and "Bright" + """ + return self._device.dimmer + + @property + def auto_standby(self) -> Optional[str]: + """ + Return the auto-standby state of the device. + + Only available if using Telnet. + + Possible values are: "OFF", "15M", "30M", "60M" + """ + return self._device.auto_standby + + @property + def sleep(self) -> Optional[Union[str, int]]: + """ + Return the sleep timer for the device. + + Only available if using Telnet. + + Possible values are: "OFF" and 1-120 (in minutes) + """ + return self._device.sleep + + @property + def delay(self) -> Optional[int]: + """ + Return the audio delay for the device in ms. + + Only available if using Telnet. + """ + return self._device.delay + + @property + def eco_mode(self) -> Optional[str]: + """ + Returns the eco-mode for the device. + + Only available if using Telnet. + + Possible values are: "Off", "On", "Auto" + """ + return self._device.eco_mode + + @property + def hdmi_output(self) -> Optional[str]: + """ + Returns the HDMI-output for the device. + + Only available if using Telnet. + + Possible values are: "Auto", "HDMI1", "HDMI2" + """ + return self._device.hdmi_output + + @property + def hdmi_audio_decode(self) -> Optional[str]: + """ + Returns the HDMI Audio Decode mode for the device. + + Only available if using Telnet. + + Possible values are: "AMP", "TV" + """ + return self._device.hdmi_audio_decode + + @property + def video_processing_mode(self) -> Optional[str]: + """ + Return the video processing mode for the device. + + Only available if using Telnet. + + Possible values are: "Auto", "Game", "Movie", "Bypass" + """ + return self._device.video_processing_mode + + @property + def tactile_transducer(self) -> Optional[str]: + """ + Return the tactile transducer state of the device. + + Only available if using Telnet. + + Possible values are ON, OFF and Number representing the intensity + """ + return self._device.tactile_transducer + + @property + def tactile_transducer_level(self) -> Optional[float]: + """ + Return the tactile transducer level in dB. + + Only available if using Telnet. + """ + return self._device.tactile_transducer_level + + @property + def tactile_transducer_lpf(self) -> Optional[str]: + """ + Return the tactile transducer low pass filter frequency. + + Only available if using Telnet. + """ + return self._device.tactile_transducer_lpf + + @property + def room_size(self) -> Optional[str]: + """ + Return the room size for the device. + + Only available if using Telnet. + + Possible values are: "S", "MS", "M", "ML", "L" + """ + return self._device.room_size + + @property + def triggers(self) -> Dict[int, str]: + """ + Return the triggers and their statuses for the device. + + Only available if using Telnet. + """ + return self._device.triggers + + @property + def speaker_preset(self) -> Optional[int]: + """ + Return the speaker preset for the device. + + Only available if using Telnet. + + Possible values are: "1", "2" + """ + return self._device.speaker_preset + + @property + def bt_transmitter(self) -> Optional[bool]: + """ + Return the Bluetooth transmitter state for the device. + + Only available if using Telnet. + """ + return self._device.bt_transmitter + + @property + def bt_output_mode(self) -> Optional[str]: + """ + Return the Bluetooth output mode for the device. + + Only available if using Telnet. + + Possible values are: "Bluetooth + Speakers", "Bluetooth Only" + """ + return self._device.bt_output_mode + + @property + def delay_time(self) -> Optional[int]: + """ + Return the delay time for the device in ms. + + Only available if using Telnet. + """ + return self._device.delay_time + + @property + def audio_restorer(self) -> Optional[str]: + """ + Return the audio restorer for the device. + + Only available if using Telnet. + + Possible values are: "Off", "Low", "Medium", "High" + """ + return self._device.audio_restorer + + @property + def graphic_eq(self) -> Optional[bool]: + """ + Return the Graphic EQ status for the device. + + Only available if using Telnet. + """ + return self._device.graphic_eq + + @property + def headphone_eq(self) -> Optional[bool]: + """ + Return the Headphone EQ status for the device. + + Only available if using Telnet. + """ + return self._device.headphone_eq + + ########## + # Getter # + ########## + + def get_trigger(self, trigger: int) -> Optional[str]: + """ + Return the status of a specific trigger. + + Only available if using Telnet. + + Valid trigger values are 1-3. + """ + return self._device.get_trigger(trigger) + ########## # Setter # ########## @@ -722,3 +962,253 @@ async def async_options(self) -> None: async def async_settings_menu(self) -> None: """Raise settings menu to receiver via HTTP get command.""" await self._device.async_settings_menu() + + async def async_channel_level_adjust(self) -> None: + """Toggle the channel level adjust menu on receiver via HTTP get command.""" + await self._device.async_channel_level_adjust() + + async def async_dimmer_toggle(self) -> None: + """Toggle dimmer on receiver via HTTP get command.""" + await self._device.async_dimmer_toggle() + + async def async_dimmer(self, mode: DimmerModes) -> None: + """Set dimmer mode on receiver via HTTP get command.""" + await self._device.async_dimmer(mode) + + async def async_auto_standby(self, auto_standby: AutoStandbys) -> None: + """Set auto standby on receiver via HTTP get command.""" + await self._device.async_auto_standby(auto_standby) + + async def async_sleep(self, sleep: Union[Literal["OFF"], int]) -> None: + """ + Set auto standby on receiver via HTTP get command. + + Valid sleep values are "OFF" and 1-120 (in minutes) + """ + await self._device.async_sleep(sleep) + + async def async_delay_up(self) -> None: + """Increase delay of the audio.""" + await self._device.async_delay_up() + + async def async_delay_down(self) -> None: + """Decrease delay of the audio.""" + await self._device.async_delay_down() + + async def async_eco_mode(self, mode: EcoModes) -> None: + """Set Eco mode.""" + await self._device.async_eco_mode(mode) + + async def async_hdmi_output(self, output: HDMIOutputs) -> None: + """Set HDMI output.""" + await self._device.async_hdmi_output(output) + + async def async_hdmi_audio_decode(self, mode: HDMIAudioDecodes) -> None: + """Set HDMI Audio Decode mode on receiver via HTTP get command.""" + await self._device.async_hdmi_audio_decode(mode) + + async def async_video_processing_mode(self, mode: VideoProcessingModes) -> None: + """Set video processing mode on receiver via HTTP get command.""" + await self._device.async_video_processing_mode(mode) + + async def async_status(self) -> str: + """ + Toggles the display of status on the device. + + Only supported on Denon models. + """ + return await self._device.async_status() + + async def async_system_reset(self) -> None: + """DANGER! Reset the receiver via HTTP get command.""" + await self._device.async_system_reset() + + async def async_network_restart(self) -> None: + """Restart the network on the receiver via HTTP get command.""" + await self._device.async_network_restart() + + async def async_speaker_preset(self, preset: int) -> None: + """ + Set speaker preset on receiver via HTTP get command. + + Valid preset values are 1-2. + """ + await self._device.async_speaker_preset(preset) + + async def async_speaker_preset_toggle(self) -> None: + """ + Toggle speaker preset on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_speaker_preset_toggle() + + async def async_bt_transmitter_on(self) -> None: + """Turn on Bluetooth transmitter on receiver via HTTP get command.""" + await self._device.async_bt_transmitter_on() + + async def async_bt_transmitter_off(self) -> None: + """Turn off Bluetooth transmitter on receiver via HTTP get command.""" + await self._device.async_bt_transmitter_off() + + async def async_bt_transmitter_toggle(self) -> None: + """ + Toggle Bluetooth transmitter mode on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_bt_transmitter_toggle() + + async def async_bt_output_mode(self, mode: BluetoothOutputModes) -> None: + """Set Bluetooth transmitter mode on receiver via HTTP get command.""" + await self._device.async_bt_output_mode(mode) + + async def async_bt_output_mode_toggle(self) -> None: + """ + Toggle Bluetooth output mode on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_bt_output_mode_toggle() + + async def async_delay_time_up(self) -> None: + """Delay time up on receiver via HTTP get command.""" + await self._device.async_delay_time_up() + + async def async_delay_time_down(self) -> None: + """Delay time up on receiver via HTTP get command.""" + await self._device.async_delay_time_down() + + async def async_delay_time(self, delay_time: int) -> None: + """ + Set delay time on receiver via HTTP get command. + + :param delay_time: Delay time in ms. Valid values are 0-999. + """ + await self._device.async_delay_time(delay_time) + + async def async_audio_restorer(self, mode: AudioRestorers): + """Set audio restorer on receiver via HTTP get command.""" + await self._device.async_audio_restorer(mode) + + async def async_remote_control_lock(self): + """Set remote control lock on receiver via HTTP get command.""" + await self._device.async_remote_control_lock() + + async def async_remote_control_unlock(self): + """Set remote control unlock on receiver via HTTP get command.""" + await self._device.async_remote_control_unlock() + + async def async_panel_lock(self, panel_lock_mode: PanelLocks): + """Set panel lock on receiver via HTTP get command.""" + await self._device.async_panel_lock(panel_lock_mode) + + async def async_panel_unlock(self): + """Set panel unlock on receiver via HTTP get command.""" + await self._device.async_panel_unlock() + + async def async_graphic_eq_on(self) -> None: + """Turn on Graphic EQ on receiver via HTTP get command.""" + await self._device.async_graphic_eq_on() + + async def async_graphic_eq_off(self) -> None: + """Turn off Graphic EQ on receiver via HTTP get command.""" + await self._device.async_graphic_eq_off() + + async def async_graphic_eq_toggle(self) -> None: + """ + Toggle Graphic EQ on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_graphic_eq_toggle() + + async def async_headphone_eq_on(self) -> None: + """Turn on Headphone EQ on receiver via HTTP get command.""" + await self._device.async_headphone_eq_on() + + async def async_headphone_eq_off(self) -> None: + """Turn off Headphone EQ on receiver via HTTP get command.""" + await self._device.async_headphone_eq_off() + + async def async_headphone_eq_toggle(self) -> None: + """ + Toggle Headphone EQ on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_headphone_eq_toggle() + + async def async_tactile_transducer_on(self) -> None: + """Turn on tactile transducer on receiver via HTTP get command.""" + await self._device.async_tactile_transducer_on() + + async def async_tactile_transducer_off(self) -> None: + """Turn on tactile transducer on receiver via HTTP get command.""" + await self._device.async_tactile_transducer_off() + + async def async_tactile_transducer_toggle(self) -> None: + """ + Turn on tactile transducer on receiver via HTTP get command. + + Only available if using Telnet. + """ + await self._device.async_tactile_transducer_toggle() + + async def async_tactile_transducer_level_up(self) -> None: + """Increase the transducer level on receiver via HTTP get command.""" + await self._device.async_tactile_transducer_level_up() + + async def async_tactile_transducer_level_down(self) -> None: + """Decrease the transducer on receiver via HTTP get command.""" + await self._device.async_tactile_transducer_level_down() + + async def async_transducer_lpf(self, lpf: TransducerLPFs) -> None: + """Set transducer low pass filter on receiver via HTTP get command.""" + await self._device.async_transducer_lpf(lpf) + + async def async_room_size(self, room_size: RoomSizes) -> None: + """Set room size on receiver via HTTP get command.""" + await self._device.async_room_size(room_size) + + async def async_trigger_on(self, trigger: int) -> None: + """ + Set trigger to ON on receiver via HTTP get command. + + :param trigger: Trigger number to set to ON. Valid values are 1-3. + """ + await self._device.async_trigger_on(trigger) + + async def async_trigger_off(self, trigger: int) -> None: + """ + Set trigger to OFF on receiver via HTTP get command. + + :param trigger: Trigger number to set to OFF. Valid values are 1-3. + """ + await self._device.async_trigger_off(trigger) + + async def async_trigger_toggle(self, trigger: int) -> None: + """ + Toggle trigger on receiver via HTTP get command. + + Only available if using Telnet. + + :param trigger: Trigger number to toggle. Valid values are 1-3. + """ + await self._device.async_trigger_toggle(trigger) + + async def async_quick_select_mode(self, quick_select_number: int) -> None: + """ + Set quick select mode on receiver via HTTP get command. + + :param quick_select_number: Quick select number to set. Valid values are 1-5. + """ + await self._device.async_quick_select_mode(quick_select_number) + + async def async_quick_select_memory(self, quick_select_number: int) -> None: + """ + Set quick select memory on receiver via HTTP get command. + + :param quick_select_number: Quick select number to set. Valid values are 1-5. + """ + await self._device.async_quick_select_memory(quick_select_number) diff --git a/denonavr/dirac.py b/denonavr/dirac.py new file mode 100644 index 0000000..6dbb121 --- /dev/null +++ b/denonavr/dirac.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This module implements the Audyssey settings of Denon AVR receivers. + +:copyright: (c) 2020 by Oliver Goetz. +:license: MIT, see LICENSE for more details. +""" + +import logging +from typing import Optional + +import attr + +from .const import ( + DENON_ATTR_SETATTR, + DIRAC_FILTER_MAP, + DIRAC_FILTER_MAP_LABELS, + DiracFilters, +) +from .exceptions import AvrCommandError +from .foundation import DenonAVRFoundation + +_LOGGER = logging.getLogger(__name__) + + +@attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR) +class DenonAVRDirac(DenonAVRFoundation): + """Dirac Settings.""" + + _dirac_filter: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + + def setup(self) -> None: + """Ensure that the instance is initialized.""" + self._device.telnet_api.register_callback( + "PS", self._async_dirac_filter_callback + ) + self._is_setup = True + + async def _async_dirac_filter_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle Dirac filter change event.""" + if event == "PS" and parameter[0:5] == "DIRAC": + self._dirac_filter = DIRAC_FILTER_MAP_LABELS[parameter[6:]] + + ############## + # Properties # + ############## + + @property + def dirac_filter(self) -> Optional[str]: + """ + Returns the current Dirac filter. + + Only available if using Telnet. + + Possible values are: "Off", "Slot 1", "Slot 2", "Slot 3" + """ + return self._dirac_filter + + ########## + # Setter # + ########## + async def async_dirac_filter(self, dirac_filter: DiracFilters) -> None: + """Set Dirac filter.""" + if dirac_filter not in DiracFilters: + raise AvrCommandError("Invalid Dirac filter") + + mapped_filter = DIRAC_FILTER_MAP[dirac_filter] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_dirac_filter.format( + filter=mapped_filter + ) + ) + return + await self._device.api.async_get_command( + self._device.urls.command_dirac_filter.format(filter=mapped_filter) + ) + + +def dirac_factory(instance: DenonAVRFoundation) -> DenonAVRDirac: + """Create DenonAVRDirac at receiver instances.""" + # pylint: disable=protected-access + new = DenonAVRDirac(device=instance._device) + return new diff --git a/denonavr/foundation.py b/denonavr/foundation.py index cf3119b..3749842 100644 --- a/denonavr/foundation.py +++ b/denonavr/foundation.py @@ -12,7 +12,7 @@ import xml.etree.ElementTree as ET from collections.abc import Hashable from copy import deepcopy -from typing import Dict, List, Optional, Union +from typing import Dict, List, Literal, Optional, Union, get_args import attr @@ -21,31 +21,55 @@ from .const import ( APPCOMMAND_CMD_TEXT, APPCOMMAND_NAME, + AUDIO_RESTORER_MAP, + AUDIO_RESTORER_MAP_LABELS, AVR, AVR_X, AVR_X_2016, + BLUETOOTH_OUTPUT_MAP_LABELS, + BLUETOOTH_OUTPUT_MODES_MAP, + CHANNEL_VOLUME_MAP, DENON_ATTR_SETATTR, DENONAVR_TELNET_COMMANDS, DENONAVR_URLS, DESCRIPTION_TYPES, DEVICEINFO_AVR_X_PATTERN, DEVICEINFO_COMMAPI_PATTERN, + DIMMER_MODE_MAP, + DIMMER_MODE_MAP_LABELS, + ECO_MODE_MAP, + ECO_MODE_MAP_LABELS, + HDMI_OUTPUT_MAP_LABELS, MAIN_ZONE, POWER_STATES, SETTINGS_MENU_STATES, VALID_RECEIVER_TYPES, VALID_ZONES, + VIDEO_PROCESSING_MODES_MAP, + VIDEO_PROCESSING_MODES_MAP_LABELS, ZONE2, ZONE2_TELNET_COMMANDS, ZONE2_URLS, ZONE3, ZONE3_TELNET_COMMANDS, ZONE3_URLS, + AudioRestorers, + AutoStandbys, + BluetoothOutputModes, + DimmerModes, + EcoModes, + HDMIAudioDecodes, + HDMIOutputs, + PanelLocks, ReceiverType, ReceiverURLs, + RoomSizes, TelnetCommands, + TransducerLPFs, + VideoProcessingModes, ) from .exceptions import ( + AvrCommandError, AvrForbiddenError, AvrIncompleteResponseError, AvrNetworkError, @@ -58,6 +82,17 @@ _LOGGER = logging.getLogger(__name__) +def convert_on_off_bool(value: str) -> Optional[bool]: + """Convert a ON/OFF string to bool.""" + if value is None: + return None + if value.lower() == "on": + return True + if value.lower() == "off": + return False + return None + + @attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR) class DenonAVRDeviceInfo: """Implements a class with device information of the receiver.""" @@ -103,9 +138,75 @@ class DenonAVRDeviceInfo: _power: Optional[str] = attr.ib( converter=attr.converters.optional(str), default=None ) - _settings_menu: Optional[str] = attr.ib( + _settings_menu: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _dimmer: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _auto_standby: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _auto_standbys = get_args(AutoStandbys) + _sleep: Optional[Union[str, int]] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _delay: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _eco_mode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _hdmi_output: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _hdmi_audio_decode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _hdmi_audio_decodes = get_args(HDMIAudioDecodes) + _video_processing_mode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _video_processing_modes = get_args(VideoProcessingModes) + _tactile_transducer: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _tactile_transducer_level: Optional[float] = attr.ib( + converter=attr.converters.optional(float), default=None + ) + _tactile_transducer_lpf: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _tactile_transducer_lpfs = get_args(TransducerLPFs) + _room_size: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _room_sizes = get_args(RoomSizes) + _triggers: Optional[Dict[int, str]] = attr.ib(default=None) + _speaker_preset: Optional[int] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _bt_transmitter: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _bt_output_mode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _bt_output_modes = get_args(BluetoothOutputModes) + _delay_time: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _audio_restorer: Optional[str] = attr.ib( converter=attr.converters.optional(str), default=None ) + _audio_restorers = get_args(AudioRestorers) + _panel_locks = get_args(PanelLocks) + _graphic_eq: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _headphone_eq: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) _is_setup: bool = attr.ib(converter=bool, default=False, init=False) _allow_recovery: bool = attr.ib(converter=bool, default=True, init=True) _setup_lock: asyncio.Lock = attr.ib(default=attr.Factory(asyncio.Lock)) @@ -143,6 +244,170 @@ async def _async_settings_menu_callback( ): self._settings_menu = parameter[4:] + async def _async_dimmer_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a dimmer change event.""" + if event == "DIM" and parameter[1:] in DIMMER_MODE_MAP_LABELS: + self._dimmer = DIMMER_MODE_MAP_LABELS[parameter[1:]] + + async def _async_auto_standby_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a auto standby change event.""" + if zone == "Main" and event == "STBY": + self._auto_standby = parameter + + async def _async_auto_sleep_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a sleep change event.""" + if event != "SLP": + return + + if parameter == "OFF": + self._sleep = parameter + else: + self._sleep = int(parameter) + + async def _async_room_size_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a room size change event.""" + if parameter[:3] != "RSZ": + return + + self._room_size = parameter[4:] + + async def _async_trigger_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a trigger change event.""" + if event != "TR": + return + + values = parameter.split() + if len(values) != 2: + return + + if self._triggers is None: + self._triggers = {} + + self._triggers[int(values[0])] = values[1] + + async def _async_delay_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a delay change event.""" + if event == "PS" and parameter[0:3] == "DEL": + self._delay = int(parameter[4:]) + + async def _async_eco_mode_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle an Eco-mode change event.""" + if event == "ECO" and parameter in ECO_MODE_MAP_LABELS: + self._eco_mode = ECO_MODE_MAP_LABELS[parameter] + + async def _async_hdmi_output_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a HDMI output change event.""" + if event == "VS" and parameter[0:4] == "MONI": + self._hdmi_output = HDMI_OUTPUT_MAP_LABELS[parameter] + + async def _async_hdmi_audio_decode_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a HDMI Audio Decode mode change event.""" + if event == "VS" and parameter[0:5] == "AUDIO": + self._hdmi_audio_decode = parameter[6:] + + async def _async_video_processing_mode_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Video Processing Mode change event.""" + if event == "VS" and parameter[0:3] == "VPM": + self._video_processing_mode = VIDEO_PROCESSING_MODES_MAP_LABELS[ + parameter[3:] + ] + + async def _async_tactile_transducer_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a tactile transducer change event.""" + key_value = parameter.split() + if len(key_value) != 2 or parameter[0:3] != "TTR": + return + + key = key_value[0] + value = key_value[1] + if value == "END": + return + + if key == "TTR": + self._tactile_transducer = value + elif key == "TTRLEV": + self._tactile_transducer_level = CHANNEL_VOLUME_MAP[value] + elif key == "TTRLPF": + self._tactile_transducer_lpf = f"{int(value)} Hz" + + async def _async_speaker_preset_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a speaker preset change event.""" + if event != "SP": + return + + if parameter[0:2] == "PR": + self._speaker_preset = int(parameter[3:]) + + async def _async_bt_callback(self, zone: str, event: str, parameter: str) -> None: + """Handle a Bluetooth change event.""" + if event != "BT" or parameter[0:2] != "TX": + return + + if parameter[3:] in ("ON", "OFF"): + self._bt_transmitter = parameter[3:] + else: + self._bt_output_mode = BLUETOOTH_OUTPUT_MAP_LABELS[parameter[3:]] + + async def _async_delay_time_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a delay time change event.""" + if event != "PS" or parameter[0:3] != "DEL": + return + + self._delay_time = int(parameter[4:]) + + async def _async_audio_restorer_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle an audio restorer change event.""" + if event != "PS" or parameter[0:4] != "RSTR": + return + + self._audio_restorer = AUDIO_RESTORER_MAP_LABELS[parameter[5:]] + + async def _async_graphic_eq_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Graphic EQ change event.""" + if parameter[0:3] != "GEQ": + return + + self._graphic_eq = parameter[4:] + + async def _async_headphone_eq_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Headphone EQ change event.""" + if parameter[0:3] != "HEQ": + return + + self._headphone_eq = parameter[4:] + def get_own_zone(self) -> str: """ Get zone from actual instance. @@ -183,6 +448,29 @@ async def async_setup(self) -> None: self.telnet_api.register_callback(power_event, self._async_power_callback) self.telnet_api.register_callback("MN", self._async_settings_menu_callback) + self.telnet_api.register_callback("DIM", self._async_dimmer_callback) + self.telnet_api.register_callback("PS", self._async_delay_callback) + self.telnet_api.register_callback("ECO", self._async_eco_mode_callback) + self.telnet_api.register_callback("VS", self._async_hdmi_output_callback) + self.telnet_api.register_callback( + "VS", self._async_hdmi_audio_decode_callback + ) + self.telnet_api.register_callback( + "VS", self._async_video_processing_mode_callback + ) + self.telnet_api.register_callback( + "SS", self._async_tactile_transducer_callback + ) + self.telnet_api.register_callback("STBY", self._async_auto_standby_callback) + self.telnet_api.register_callback("SLP", self._async_auto_sleep_callback) + self.telnet_api.register_callback("PS", self._async_room_size_callback) + self.telnet_api.register_callback("TR", self._async_trigger_callback) + self.telnet_api.register_callback("SP", self._async_speaker_preset_callback) + self.telnet_api.register_callback("BT", self._async_bt_callback) + self.telnet_api.register_callback("PS", self._async_delay_time_callback) + self.telnet_api.register_callback("PS", self._async_audio_restorer_callback) + self.telnet_api.register_callback("PS", self._async_graphic_eq_callback) + self.telnet_api.register_callback("PS", self._async_headphone_eq_callback) self._is_setup = True _LOGGER.debug("Finished device setup") @@ -539,19 +827,242 @@ def power(self) -> Optional[str]: return self._power @property - def settings_menu(self) -> Optional[str]: + def settings_menu(self) -> Optional[bool]: """ - Return the settings menu state of the device. Only available if using Telnet. + Returns the settings menu state of the device. - Possible values are: "ON" and "OFF" + Only available if using Telnet. """ return self._settings_menu + @property + def dimmer(self) -> Optional[str]: + """ + Returns the dimmer state of the device. + + Only available if using Telnet. + + Possible values are: "Off", "Dark", "Dim" and "Bright" + """ + return self._dimmer + + @property + def auto_standby(self) -> Optional[str]: + """ + Return the auto-standby state of the device. + + Only available if using Telnet. + + Possible values are: "OFF", "15M", "30M", "60M" + """ + return self._auto_standby + + @property + def sleep(self) -> Optional[Union[str, int]]: + """ + Return the sleep timer for the device. + + Only available if using Telnet. + + Possible values are: "OFF" and 1-120 (in minutes) + """ + return self._sleep + + @property + def delay(self) -> Optional[int]: + """ + Return the audio delay for the device in ms. + + Only available if using Telnet. + """ + return self._delay + + @property + def eco_mode(self) -> Optional[str]: + """ + Returns the eco-mode for the device. + + Only available if using Telnet. + + Possible values are: "Off", "On", "Auto" + """ + return self._eco_mode + + @property + def hdmi_output(self) -> Optional[str]: + """ + Returns the HDMI-output for the device. + + Only available if using Telnet. + + Possible values are: "Auto", "HDMI1", "HDMI2" + """ + return self._hdmi_output + + @property + def hdmi_audio_decode(self) -> Optional[str]: + """ + Returns the HDMI Audio Decode for the device. + + Only available if using Telnet. + + Possible values are: "AMP", "TV" + """ + return self._hdmi_audio_decode + + @property + def video_processing_mode(self) -> Optional[str]: + """ + Return the video processing mode for the device. + + Only available if using Telnet. + + Possible values are: "Auto", "Game", "Movie", "Bypass" + """ + return self._video_processing_mode + + @property + def tactile_transducer(self) -> Optional[str]: + """ + Return the tactile transducer state of the device. + + Only available if using Telnet. + + Possible values are ON, OFF and Number representing the intensity + """ + return self._tactile_transducer + + @property + def tactile_transducer_level(self) -> Optional[float]: + """ + Return the tactile transducer level in dB. + + Only available if using Telnet. + """ + return self._tactile_transducer_level + + @property + def tactile_transducer_lpf(self) -> Optional[str]: + """ + Return the tactile transducer low pass filter frequency. + + Only available if using Telnet. + """ + return self._tactile_transducer_lpf + + @property + def room_size(self) -> Optional[str]: + """ + Return the room size for the device. + + Only available if using Telnet. + + Possible values are: "S", "MS", "M", "ML", "L" + """ + return self._room_size + + @property + def triggers(self) -> Dict[int, str]: + """ + Return the triggers and their statuses for the device. + + Only available if using Telnet. + """ + return self._triggers + + @property + def speaker_preset(self) -> Optional[int]: + """ + Return the speaker preset for the device. + + Only available if using Telnet. + + Possible values are: "1", "2" + """ + return self._speaker_preset + + @property + def bt_transmitter(self) -> Optional[bool]: + """ + Return the Bluetooth transmitter state for the device. + + Only available if using Telnet. + """ + return self._bt_transmitter + + @property + def bt_output_mode(self) -> Optional[str]: + """ + Return the Bluetooth output mode for the device. + + Only available if using Telnet. + + Possible values are: "Bluetooth + Speakers", "Bluetooth Only" + """ + return self._bt_output_mode + + @property + def delay_time(self) -> Optional[int]: + """ + Return the delay time for the device in ms. + + Only available if using Telnet. + """ + return self._delay_time + + @property + def audio_restorer(self) -> Optional[str]: + """ + Return the audio restorer for the device. + + Only available if using Telnet. + + Possible values are: "Off", "Low", "Medium", "High" + """ + return self._audio_restorer + + @property + def graphic_eq(self) -> Optional[bool]: + """ + Return the Graphic EQ status for the device. + + Only available if using Telnet. + """ + return self._graphic_eq + + @property + def headphone_eq(self) -> Optional[bool]: + """ + Return the Headphone EQ status for the device. + + Only available if using Telnet. + """ + return self._headphone_eq + @property def telnet_available(self) -> bool: """Return true if telnet is connected and healthy.""" return self.telnet_api.connected and self.telnet_api.healthy + ########## + # Getter # + ########## + + def get_trigger(self, trigger: int) -> Optional[str]: + """ + Return the status of a specific trigger. + + Only available if using Telnet. + + Valid trigger values are 1-3. + """ + if trigger < 1 or trigger > 3: + raise AvrCommandError(f"Invalid trigger {trigger}, must be between 1 and 3") + + if self._triggers is None: + return None + return self._triggers.get(trigger) + ########## # Setter # ########## @@ -647,24 +1158,639 @@ async def async_options(self) -> None: await self.api.async_get_command(self.urls.command_options) async def async_settings_menu(self) -> None: - """Options menu on receiver via HTTP get command.""" - # fast path if we already know the state + """ + Options menu on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self._settings_menu: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_setup_close + ) + else: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_setup_open + ) + + async def async_channel_level_adjust(self) -> None: + """Toggle the channel level adjust menu on receiver via HTTP get command.""" if self.telnet_available: - if self._settings_menu == "ON": - await self.telnet_api.async_send_commands( - self.telnet_commands.command_setup_close - ) - else: - await self.telnet_api.async_send_commands( - self.telnet_commands.command_setup_open - ) - return - # slow path if we need to query the state - res = await self.api.async_get_command(self.urls.command_setup_query) - if res is not None and res == "MNMEN ON": - await self.api.async_get_command(self.urls.command_setup_close) + await self.telnet_api.async_send_commands( + self.telnet_commands.command_channel_level_adjust + ) + else: + await self.api.async_get_command(self.urls.command_channel_level_adjust) + + async def async_dimmer_toggle(self) -> None: + """Toggle dimmer on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_dimmer_toggle + ) + else: + await self.api.async_get_command(self.urls.command_dimmer_toggle) + + async def async_dimmer(self, mode: DimmerModes) -> None: + """Set dimmer mode on receiver via HTTP get command.""" + if mode not in DimmerModes: + raise AvrCommandError("Invalid dimmer mode") + + mapped_mode = DIMMER_MODE_MAP[mode] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_dimmer_set.format(mode=mapped_mode) + ) + else: + await self.api.async_get_command( + self.urls.command_dimmer_set.format(mode=mapped_mode) + ) + + async def async_tactile_transducer_on(self) -> None: + """Turn on tactile transducer on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_tactile_transducer.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_tactile_transducer.format(mode="ON") + ) + + async def async_auto_standby(self, auto_standby: AutoStandbys) -> None: + """Set auto standby on receiver via HTTP get command.""" + if auto_standby not in self._auto_standbys: + raise AvrCommandError("Invalid Auto Standby mode") + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_auto_standby.format(mode=auto_standby) + ) + else: + await self.api.async_get_command( + self.urls.command_auto_standby.format(mode=auto_standby) + ) + + async def async_sleep(self, sleep: Union[Literal["OFF"], int]) -> None: + """ + Set auto standby on receiver via HTTP get command. + + Valid sleep values are "OFF" and 1-120 (in minutes) + """ + if sleep != "OFF" and sleep not in range(1, 120): + raise AvrCommandError("Invalid sleep value") + + local_sleep = f"{sleep:03}" if isinstance(sleep, int) else sleep + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_sleep.format(value=local_sleep) + ) + else: + await self.api.async_get_command( + self.urls.command_sleep.format(value=local_sleep) + ) + + async def async_tactile_transducer_off(self) -> None: + """Turn on tactile transducer on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_tactile_transducer.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_tactile_transducer.format(mode="OFF") + ) + + async def async_tactile_transducer_toggle(self) -> None: + """ + Turn on tactile transducer on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self._tactile_transducer != "OFF": + await self.async_tactile_transducer_off() + else: + await self.async_tactile_transducer_on() + + async def async_tactile_transducer_level_up(self) -> None: + """Increase the transducer level on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_tactile_transducer_level.format(mode="UP") + ) + else: + await self.api.async_get_command( + self.urls.command_tactile_transducer_level.format(mode="UP") + ) + + async def async_tactile_transducer_level_down(self) -> None: + """Decrease the transducer level on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_tactile_transducer_level.format( + mode="DOWN" + ) + ) + else: + await self.api.async_get_command( + self.urls.command_tactile_transducer_level.format(mode="DOWN") + ) + + async def async_transducer_lpf(self, lpf: TransducerLPFs): + """Set transducer low pass filter on receiver via HTTP get command.""" + if lpf not in self._tactile_transducer_lpfs: + raise AvrCommandError("Invalid tactile transducer low pass filter") + + frequency = lpf.split()[0] + if len(frequency) == 2: + frequency = f"0{frequency}" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_tactile_transducer_lpf.format( + frequency=frequency + ) + ) + else: + await self.api.async_get_command( + self.urls.command_tactile_transducer_lpf.format(frequency=frequency) + ) + + async def async_room_size(self, room_size: RoomSizes) -> None: + """Set room size on receiver via HTTP get command.""" + if room_size not in self._room_sizes: + raise AvrCommandError("Invalid room size") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_room_size.format(size=room_size) + ) + else: + await self.api.async_get_command( + self.urls.command_room_size.format(size=room_size) + ) + + async def async_trigger_on(self, trigger: int) -> None: + """ + Set trigger to ON on receiver via HTTP get command. + + :param trigger: Trigger number to set to ON. Valid values are 1-3. + """ + if trigger < 1 or trigger > 3: + raise AvrCommandError("Trigger number must be between 1 and 3") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_trigger.format(number=trigger, mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_trigger.format(number=trigger, mode="ON") + ) + + async def async_trigger_off(self, trigger: int) -> None: + """ + Set trigger to OFF on receiver via HTTP get command. + + :param trigger: Trigger number to set to OFF. Valid values are 1-3. + """ + if trigger < 1 or trigger > 3: + raise AvrCommandError("Trigger number must be between 1 and 3") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_trigger.format(number=trigger, mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_trigger.format(number=trigger, mode="OFF") + ) + + async def async_trigger_toggle(self, trigger: int) -> None: + """ + Toggle trigger on receiver via HTTP get command. + + Only available if using Telnet. + + :param trigger: Trigger number to toggle. Valid values are 1-3. + """ + if trigger < 1 or trigger > 3: + raise AvrCommandError("Trigger number must be between 1 and 3") + + trigger_status = self._triggers.get(trigger) + if trigger_status == "ON": + await self.async_trigger_off(trigger) + else: + await self.async_trigger_on(trigger) + + async def async_quick_select_mode(self, quick_select_number: int) -> None: + """ + Set quick select mode on receiver via HTTP get command. + + :param quick_select_number: Quick select number to set. Valid values are 1-5. + """ + if quick_select_number not in range(1, 5): + raise AvrCommandError("Quick select number must be between 1 and 5") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_quick_select_mode.format( + number=quick_select_number + ) + ) + else: + await self.api.async_get_command( + self.urls.command_quick_select_mode.format(number=quick_select_number) + ) + + async def async_quick_select_memory(self, quick_select_number: int) -> None: + """ + Set quick select memory on receiver via HTTP get command. + + :param quick_select_number: Quick select number to set. Valid values are 1-5. + """ + if quick_select_number not in range(1, 5): + raise AvrCommandError("Quick select number must be between 1 and 5") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_quick_select_memory.format( + number=quick_select_number + ) + ) + else: + await self.api.async_get_command( + self.urls.command_quick_select_memory.format(number=quick_select_number) + ) + + async def async_delay_up(self) -> None: + """Delay up on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_delay_up + ) + else: + await self.api.async_get_command(self.urls.command_delay_up) + + async def async_delay_down(self) -> None: + """Delay down on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_delay_down + ) + else: + await self.api.async_get_command(self.urls.command_delay_down) + + async def async_eco_mode(self, mode: EcoModes) -> None: + """Set Eco mode.""" + if mode not in EcoModes: + raise AvrCommandError("Invalid Eco mode") + + mapped_mode = ECO_MODE_MAP[mode] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_eco_mode.format(mode=mapped_mode) + ) + else: + await self.api.async_get_command( + self.urls.command_eco_mode.format(mode=mapped_mode) + ) + + async def async_hdmi_output(self, output: HDMIOutputs) -> None: + """Set HDMI output.""" + if output not in HDMIOutputs: + raise AvrCommandError("Invalid HDMI output mode") + + mapped_output = HDMIOutputs[output] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_hdmi_output.format(output=mapped_output) + ) + else: + await self.api.async_get_command( + self.urls.command_hdmi_output.format(output=mapped_output) + ) + + async def async_hdmi_audio_decode(self, mode: HDMIAudioDecodes) -> None: + """Set HDMI Audio Decode mode on receiver via HTTP get command.""" + if mode not in self._hdmi_audio_decodes: + raise AvrCommandError("Invalid HDMI Audio Decode mode") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_hdmi_audio_decode.format(mode=mode) + ) + else: + await self.api.async_get_command( + self.urls.command_hdmi_audio_decode.format(mode=mode) + ) + + async def async_video_processing_mode(self, mode: VideoProcessingModes) -> None: + """Set video processing mode on receiver via HTTP get command.""" + if mode not in self._video_processing_modes: + raise AvrCommandError("Invalid video processing mode") + processing_mode = VIDEO_PROCESSING_MODES_MAP[mode] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_video_processing_mode.format( + mode=processing_mode + ) + ) + else: + await self.api.async_get_command( + self.urls.command_video_processing_mode.format(mode=processing_mode) + ) + + async def async_status(self) -> str: + """Get status of receiver via HTTP get command.""" + if "denon" not in self.manufacturer.lower(): + raise AvrCommandError("Status command is only supported for Denon devices") + return await self.api.async_get_command(self.urls.command_status) + + async def async_system_reset(self) -> None: + """DANGER! Reset the receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_system_reset + ) + else: + await self.api.async_get_command(self.urls.command_system_reset) + + async def async_network_restart(self) -> None: + """Restart the network on the receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_network_restart + ) + else: + await self.api.async_get_command(self.urls.command_network_restart) + + async def async_speaker_preset(self, preset: int) -> None: + """ + Set speaker preset on receiver via HTTP get command. + + Valid preset values are 1-2. + """ + if preset < 1 or preset > 2: + raise AvrCommandError("Speaker preset number must be 1 or 2") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_speaker_preset.format(number=preset) + ) + else: + await self.api.async_get_command( + self.urls.command_speaker_preset.format(number=preset) + ) + + async def async_speaker_preset_toggle(self) -> None: + """ + Toggle speaker preset on receiver via HTTP get command. + + Only available if using Telnet. + """ + speaker_preset = 1 if self._speaker_preset == 2 else 2 + await self.async_speaker_preset(speaker_preset) + + async def async_bt_transmitter_on( + self, + ) -> None: + """Turn on Bluetooth transmitter on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_bluetooth_transmitter.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_bluetooth_transmitter.format(mode="ON") + ) + + async def async_bt_transmitter_off( + self, + ) -> None: + """Turn off Bluetooth transmitter on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_bluetooth_transmitter.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_bluetooth_transmitter.format(mode="OFF") + ) + + async def async_bt_transmitter_toggle(self) -> None: + """ + Toggle Bluetooth transmitter mode on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self.bt_transmitter: + await self.async_bt_transmitter_off() + else: + await self.async_bt_transmitter_on() + + async def async_bt_output_mode(self, mode: BluetoothOutputModes) -> None: + """Set Bluetooth transmitter mode on receiver via HTTP get command.""" + if mode not in self._bt_output_modes: + raise AvrCommandError("Invalid Bluetooth output mode") + + mapped_mode = BLUETOOTH_OUTPUT_MODES_MAP[mode] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_bluetooth_transmitter.format( + mode=mapped_mode + ) + ) + else: + await self.api.async_get_command( + self.urls.command_bluetooth_transmitter.format(mode=mapped_mode) + ) + + async def async_bt_output_mode_toggle(self) -> None: + """ + Toggle Bluetooth output mode on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self.bt_output_mode == "Bluetooth + Speakers": + await self.async_bt_output_mode("Bluetooth Only") + else: + await self.async_bt_output_mode("Bluetooth + Speakers") + + async def async_delay_time_up(self) -> None: + """Delay time up on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_delay_time.format(value="UP") + ) + else: + await self.api.async_get_command( + self.urls.command_delay_time.format(value="UP") + ) + + async def async_delay_time_down(self) -> None: + """Delay time up on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_delay_time.format(value="DOWN") + ) + else: + await self.api.async_get_command( + self.urls.command_delay_time.format(value="DOWN") + ) + + async def async_delay_time(self, delay_time: int) -> None: + """ + Set delay time on receiver via HTTP get command. + + :param delay_time: Delay time in ms. Valid values are 0-999. + """ + if delay_time < 0 or delay_time > 999: + raise AvrCommandError("Invalid delay time value") + + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_delay_time.format(value=delay_time) + ) + else: + await self.api.async_get_command( + self.urls.command_delay_time.format(value=delay_time) + ) + + async def async_audio_restorer(self, mode: AudioRestorers): + """Set audio restorer on receiver via HTTP get command.""" + if mode not in self._audio_restorers: + raise AvrCommandError("Invalid audio restorer mode") + + mapped_mode = AUDIO_RESTORER_MAP[mode] + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_audio_restorer.format(mode=mapped_mode) + ) + else: + await self.api.async_get_command( + self.urls.command_audio_restorer.format(mode=mapped_mode) + ) + + async def async_remote_control_lock(self): + """Set remote control lock on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_remote_control_lock.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_remote_control_lock.format(mode="ON") + ) + + async def async_remote_control_unlock(self): + """Set remote control unlock on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_remote_control_lock.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_remote_control_lock.format(mode="OFF") + ) + + async def async_panel_lock(self, panel_lock_mode: PanelLocks): + """Set panel lock on receiver via HTTP get command.""" + if panel_lock_mode not in self._panel_locks: + raise AvrCommandError("Invalid panel lock mode") + + if self.telnet_available: + if panel_lock_mode == "Panel": + await self.telnet_api.async_send_commands( + self.telnet_commands.command_panel_lock.format(mode="ON") + ) + else: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_panel_and_volume_lock + ) + else: + if panel_lock_mode == "Panel": + await self.api.async_get_command( + self.urls.command_panel_lock.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_panel_and_volume_lock + ) + + async def async_panel_unlock(self): + """Set panel unlock on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_panel_lock.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_panel_lock.format(mode="OFF") + ) + + async def async_graphic_eq_on(self) -> None: + """Turn on Graphic EQ on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_graphic_eq.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_graphic_eq.format(mode="ON") + ) + + async def async_graphic_eq_off(self) -> None: + """Turn off Graphic EQ on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_graphic_eq.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_graphic_eq.format(mode="OFF") + ) + + async def async_graphic_eq_toggle(self) -> None: + """ + Toggle Graphic EQ on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self._graphic_eq: + await self.async_graphic_eq_off() + else: + await self.async_graphic_eq_on() + + async def async_headphone_eq_on(self) -> None: + """Turn on Headphone EQ on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_headphone_eq.format(mode="ON") + ) + else: + await self.api.async_get_command( + self.urls.command_headphone_eq.format(mode="ON") + ) + + async def async_headphone_eq_off(self) -> None: + """Turn off Headphone EQ on receiver via HTTP get command.""" + if self.telnet_available: + await self.telnet_api.async_send_commands( + self.telnet_commands.command_headphone_eq.format(mode="OFF") + ) + else: + await self.api.async_get_command( + self.urls.command_headphone_eq.format(mode="OFF") + ) + + async def async_headphone_eq_toggle(self) -> None: + """ + Toggle Headphone EQ on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self._headphone_eq: + await self.async_headphone_eq_off() else: - await self.api.async_get_command(self.urls.command_setup_open) + await self.async_headphone_eq_on() @attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR) diff --git a/denonavr/soundmode.py b/denonavr/soundmode.py index 2adc5d1..34400d0 100644 --- a/denonavr/soundmode.py +++ b/denonavr/soundmode.py @@ -11,14 +11,33 @@ import logging from collections.abc import Hashable from copy import deepcopy -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional, get_args import attr from .appcommand import AppCommands -from .const import ALL_ZONE_STEREO, DENON_ATTR_SETATTR, SOUND_MODE_MAPPING +from .const import ( + ALL_ZONE_STEREO, + AURO_3D_MODE_MAP, + AURO_3D_MODE_MAP_MAP_LABELS, + AURO_MATIC_3D_PRESET_MAP, + AURO_MATIC_3D_PRESET_MAP_LABELS, + DENON_ATTR_SETATTR, + DIALOG_ENHANCER_LEVEL_MAP, + DIALOG_ENHANCER_LEVEL_MAP_LABELS, + EFFECT_SPEAKER_SELECTION_MAP, + EFFECT_SPEAKER_SELECTION_MAP_LABELS, + SOUND_MODE_MAPPING, + Auro3DModes, + AuroMatic3DPresets, + DialogEnhancerLevels, + DRCs, + EffectSpeakers, + IMAXHPFs, + IMAXLPFs, +) from .exceptions import AvrCommandError, AvrIncompleteResponseError, AvrProcessingError -from .foundation import DenonAVRFoundation +from .foundation import DenonAVRFoundation, convert_on_off_bool _LOGGER = logging.getLogger(__name__) @@ -67,6 +86,65 @@ class DenonAVRSoundMode(DenonAVRFoundation): _sound_mode_raw: Optional[str] = attr.ib( converter=attr.converters.optional(convert_sound_mode), default=None ) + _neural_x: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _imax_auto_off: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _imax_audio_settings: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _imax_hpf: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _imax_hpfs = get_args(IMAXHPFs) + _imax_lpf: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _imax_lpfs = get_args(IMAXLPFs) + _imax_subwoofer_mode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _imax_subwoofer_output: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _cinema_eq: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _center_spread: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _loudness_management: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _dialog_enhancer_level: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _dialog_enhancer_levels = get_args(DialogEnhancerLevels) + _auromatic_3d_preset: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _auromatic_3d_presets = get_args(AuroMatic3DPresets) + _auromatic_3d_strength: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _auro_3d_mode: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _auro_3d_modes = get_args(Auro3DModes) + _dialog_control: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) + _speaker_virtualizer: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _effect_speaker_selection: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None + ) + _effect_speakers = get_args(EffectSpeakers) + _drc: Optional[str] = attr.ib(converter=attr.converters.optional(str), default=None) + _drcs = get_args(DRCs) _sound_mode_map: Dict[str, list] = attr.ib( validator=attr.validators.deep_mapping( attr.validators.instance_of(str), @@ -113,6 +191,33 @@ async def async_setup(self) -> None: self._device.telnet_api.register_callback( "MS", self._async_soundmode_callback ) + self._device.telnet_api.register_callback( + "PS", self._async_neural_x_callback + ) + self._device.telnet_api.register_callback("PS", self._async_imax_callback) + self._device.telnet_api.register_callback( + "PS", self._async_cinema_eq_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_center_spread_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_loudness_management_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_dialog_enhancer_callback + ) + self._device.telnet_api.register_callback("PS", self._async_auro_callback) + self._device.telnet_api.register_callback( + "PS", self._async_dialog_control_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_speaker_virtualizer_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_effect_speaker_selection_callback + ) + self._device.telnet_api.register_callback("PS", self._async_drc_callback) self._is_setup = True _LOGGER.debug("Finished sound mode setup") @@ -126,6 +231,116 @@ async def _async_soundmode_callback( self._sound_mode_raw = parameter + async def _async_neural_x_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Neural X:change event.""" + parameter_name_length = len("NEURAL") + if parameter[:parameter_name_length] == "NEURAL": + self._neural_x = parameter[parameter_name_length + 1 :] + + async def _async_imax_callback(self, zone: str, event: str, parameter: str) -> None: + """Handle an IMAX change event.""" + key_value = parameter.split() + if len(key_value) != 2 or key_value[0][:4] != "IMAX": + return + + if key_value[0] == "IMAX": + self._imax_auto_off = parameter[5:] + elif key_value[0] == "IMAXAUD": + self._imax_audio_settings = parameter[8:] + elif key_value[0] == "IMAXHPF": + self._imax_hpf = int(parameter[8:]) + elif key_value[0] == "IMAXLPF": + self._imax_lpf = int(parameter[8:]) + elif key_value[0] == "IMAXSWM": + self._imax_subwoofer_mode = parameter[8:] + elif key_value[0] == "IMAXSWO": + self._imax_subwoofer_output = parameter[8:] + + async def _async_cinema_eq_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Cinema EQ change event.""" + if parameter[:10] == "CINEMA EQ.": + self._cinema_eq = parameter[10:] + + async def _async_center_spread_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Center Spread change event.""" + if parameter[:3] == "CES": + self._center_spread = parameter[4:] + + async def _async_loudness_management_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Loudness Management change event.""" + if parameter[:3] == "LOM": + self._loudness_management = parameter[4:] + + async def _async_dialog_enhancer_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Dialog Enhancer change event.""" + if parameter[:3] == "DEH": + self._dialog_enhancer_level = DIALOG_ENHANCER_LEVEL_MAP_LABELS[ + parameter[4:] + ] + + async def _async_auro_callback(self, zone: str, event: str, parameter: str) -> None: + """Handle a Auro change event.""" + key_value = parameter.split() + if len(key_value) != 2 or key_value[0][:4] != "AURO": + return + + if key_value[0] == "AUROPR": + self._auromatic_3d_preset = AURO_MATIC_3D_PRESET_MAP_LABELS[parameter[7:]] + elif key_value[0] == "AUROST": + self._auromatic_3d_strength = int(parameter[7:]) + elif key_value[0] == "AUROMODE": + self._auro_3d_mode = AURO_3D_MODE_MAP_MAP_LABELS[parameter[9:]] + + async def _async_dialog_control_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Dialog Control change event.""" + key_value = parameter.split() + if len(key_value) != 2 or key_value[0] != "DIC": + return + + self._dialog_control = int(key_value[1]) + + async def _async_speaker_virtualizer_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Speaker Virtualizer change event.""" + key_value = parameter.split() + if len(key_value) != 2 or key_value[0] != "SPV": + return + + self._speaker_virtualizer = key_value[1] + + async def _async_effect_speaker_selection_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a Effect Speaker Selection change event.""" + key_value = parameter.split(":") + if len(key_value) != 2 or key_value[0] != "SP": + return + + self._effect_speaker_selection = EFFECT_SPEAKER_SELECTION_MAP_LABELS[ + key_value[1] + ] + + async def _async_drc_callback(self, zone: str, event: str, parameter: str) -> None: + """Handle a DRC change event.""" + key_value = parameter.split() + if len(key_value) != 2 or key_value[0] != "DRC": + return + + self._drc = key_value[1] + async def async_update( self, global_update: bool = False, cache_id: Optional[Hashable] = None ) -> None: @@ -292,6 +507,186 @@ def sound_mode_raw(self) -> Optional[str]: """Return the current sound mode as string as received from the AVR.""" return self._sound_mode_raw + @property + def neural_x(self) -> Optional[bool]: + """ + Return the current Neural:X status. + + Only available if using Telnet. + """ + return self._neural_x + + @property + def imax(self) -> Optional[str]: + """ + Return the current IMAX status. + + Only available if using Telnet. + + Possible values are: "AUTO", "OFF" + """ + return self._imax_auto_off + + @property + def imax_audio_settings(self) -> Optional[str]: + """ + Return the current IMAX Audio Settings. + + Only available if using Telnet. + + Possible values are: "AUTO", "MANUAL" + """ + return self._imax_audio_settings + + @property + def imax_hpf(self) -> Optional[int]: + """ + Return the current IMAX High Pass Filter. + + Only available if using Telnet. + """ + return self._imax_hpf + + @property + def imax_lpf(self) -> Optional[int]: + """ + Return the current IMAX Low Pass Filter. + + Only available if using Telnet. + """ + return self._imax_lpf + + @property + def imax_subwoofer_mode(self) -> Optional[str]: + """ + Return the current IMAX Subwoofer Mode. + + Only available if using Telnet. + """ + return self._imax_subwoofer_mode + + @property + def imax_subwoofer_output(self) -> Optional[str]: + """ + Return the current IMAX Subwoofer Output Mode. + + Only available if using Telnet. + """ + return self._imax_subwoofer_output + + @property + def cinema_eq(self) -> Optional[bool]: + """ + Return the current Cinema EQ status. + + Only available if using Telnet. + """ + return self._cinema_eq + + @property + def center_spread(self) -> Optional[bool]: + """ + Return the current Center Spread status. + + Only available if using Telnet. + """ + return self._center_spread + + @property + def loudness_management(self) -> Optional[bool]: + """ + Return the current Loudness Management status. + + Only available if using Telnet. + """ + return self._loudness_management + + @property + def dialog_enhancer(self) -> Optional[str]: + """ + Return the current Dialog Enhancer level. + + Only available if using Telnet. + + Possible values are: "Off", "Low", "Medium", "High" + """ + return self._dialog_enhancer_level + + @property + def auromatic_3d_preset(self) -> Optional[str]: + """ + Return the current Auro-Matic 3D Preset. + + Only available if using Telnet. + + Possible values are: "Small, "Medium", "Large", "Speech", "Movie" + """ + return self._auromatic_3d_preset + + @property + def auromatic_3d_strength(self) -> Optional[int]: + """ + Return the current Auro-Matic 3D Strength. + + Only available if using Telnet. + + Possible values are: 1-16 + """ + return self._auromatic_3d_strength + + @property + def auro_3d_mode(self) -> Optional[str]: + """ + Return the current Auro 3D mode. + + Only available if using Telnet. + + Possible values are: "Direct", "Channel Expansion" + """ + return self._auro_3d_mode + + @property + def dialog_control(self) -> Optional[int]: + """ + Return the current Dialog Control level. + + Only available if using Telnet. + + Possible values are: 0-6 + """ + return self._dialog_control + + @property + def speaker_virtualizer(self) -> Optional[bool]: + """ + Return the current Speaker Virtualizer status. + + Only available if using Telnet. + """ + return self._speaker_virtualizer + + @property + def effect_speaker_selection(self) -> Optional[str]: + """ + Return the current Effect Speaker Selection. + + Only available if using Telnet. + + Possible values are: "Floor", "Height + Floor" + """ + return self._effect_speaker_selection + + @property + def drc(self) -> Optional[str]: + """ + Return the current DRC status. + + Only available if using Telnet. + + Possible values are: "AUTO", "LOW", "MID", "HI", "OFF" + """ + return self._drc + ########## # Setter # ########## @@ -325,6 +720,520 @@ async def async_set_sound_mode(self, sound_mode: str) -> None: else: await self._device.api.async_get_command(command_url) + async def async_sound_mode_next(self): + """Select next sound mode.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_sel_sound_mode + "RIGHT" + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_sel_sound_mode + "RIGHT" + ) + + async def async_sound_mode_previous(self): + """Select previous sound mode.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_sel_sound_mode + "LEFT" + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_sel_sound_mode + "LEFT" + ) + + async def async_neural_x_on(self): + """Turn on Neural:X sound mode.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_neural_x_on_off.format(mode="ON") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_neural_x_on_off.format(mode="ON") + ) + + async def async_neural_x_off(self): + """Turn off Neural:X sound mode.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_neural_x_on_off.format(mode="OFF") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_neural_x_on_off.format(mode="OFF") + ) + + async def async_neural_x_toggle(self): + """ + Toggle Neural:X sound mode. + + Only available if using Telnet. + """ + if self._neural_x: + await self.async_neural_x_off() + else: + await self.async_neural_x_on() + + async def async_imax_auto(self): + """Set IMAX sound mode to Auto.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_auto_off.format(mode="AUTO") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_auto_off.format(mode="AUTO") + ) + + async def async_imax_off(self): + """Turn off IMAX sound mode.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_auto_off.format(mode="OFF") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_auto_off.format(mode="OFF") + ) + + async def async_imax_toggle(self): + """ + Toggle IMAX sound mode between auto and off. + + Only available if using Telnet. + """ + if self._imax_auto_off != "OFF": + await self.async_imax_off() + else: + await self.async_imax_auto() + + async def async_imax_audio_settings(self, mode: Literal["AUTO", "MANUAL"]): + """Set IMAX audio settings.""" + if mode not in ["AUTO", "MANUAL"]: + raise AvrCommandError(f"{mode} is not a valid IMAX audio setting") + + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_audio_settings.format( + mode=mode + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_audio_settings.format(mode=mode) + ) + + async def async_imax_audio_settings_toggle(self) -> None: + """ + Toggle IMAX audio settings between auto and manual. + + Only available if using Telnet. + """ + if self._imax_audio_settings == "AUTO": + await self.async_imax_audio_settings("MANUAL") + else: + await self.async_imax_audio_settings("AUTO") + + async def async_imax_hpf(self, hpf: IMAXHPFs) -> None: + """Set IMAX High Pass Filter.""" + if hpf not in self._imax_hpfs: + raise AvrCommandError(f"{hpf} is not a valid IMAX high pass filter") + + local_hpf = self._padded_pass_filter(hpf) + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_hpf.format( + frequency=local_hpf + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_hpf.format(frequency=local_hpf) + ) + + async def async_imax_lpf(self, lpf: IMAXLPFs) -> None: + """Set IMAX Low Pass Filter.""" + if lpf not in self._imax_lpfs: + raise AvrCommandError(f"{lpf} is not a valid IMAX low pass filter") + + local_lpf = self._padded_pass_filter(lpf) + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_lpf.format( + frequency=local_lpf + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_lpf.format(frequency=local_lpf) + ) + + @staticmethod + def _padded_pass_filter(pass_filter: str) -> str: + return f"0{pass_filter}" if len(str(pass_filter)) == 2 else str(pass_filter) + + async def async_imax_subwoofer_mode(self, mode: Literal["ON", "OFF"]) -> None: + """Set IMAX Subwoofer Mode.""" + if mode not in ["ON", "OFF"]: + raise AvrCommandError(f"{mode} is not a valid IMAX subwoofer mode") + + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_subwoofer_mode.format( + mode=mode + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_subwoofer_mode.format(mode=mode) + ) + + async def async_imax_subwoofer_output(self, mode: Literal["L+M", "LFE"]) -> None: + """Set IMAX Subwoofer Output Mode.""" + if mode not in ["L+M", "LFE"]: + raise AvrCommandError(f"{mode} is not a valid IMAX subwoofer output mode") + + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_imax_subwoofer_output.format( + mode=mode + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_imax_subwoofer_output.format(mode=mode) + ) + + async def async_cinema_eq_on(self): + """Set Cinema EQ to ON.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_cinema_eq.format(mode="ON") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_cinema_eq.format(mode="ON") + ) + + async def async_cinema_eq_off(self): + """Set Cinema EQ to OFF.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_cinema_eq.format(mode="OFF") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_cinema_eq.format(mode="OFF") + ) + + async def async_cinema_eq_toggle(self): + """ + Toggle Cinema EQ. + + Only available if using Telnet. + """ + if self._cinema_eq: + await self.async_cinema_eq_off() + else: + await self.async_cinema_eq_on() + + async def async_center_spread_on(self): + """Set Center Spread to ON.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_center_spread.format(mode="ON") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_center_spread.format(mode="ON") + ) + + async def async_center_spread_off(self): + """Set Center Spread to ON.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_center_spread.format(mode="OFF") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_center_spread.format(mode="OFF") + ) + + async def async_center_spread_toggle(self): + """ + Toggle Center Spread. + + Only available if using Telnet. + """ + if self._center_spread: + await self.async_center_spread_off() + else: + await self.async_center_spread_on() + + async def async_loudness_management_on(self): + """Set Loudness Management to ON.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_loudness_management.format( + mode="ON" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_loudness_management.format(mode="ON") + ) + + async def async_loudness_management_off(self): + """Set Loudness Management to OFF.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_loudness_management.format( + mode="OFF" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_loudness_management.format(mode="OFF") + ) + + async def async_loudness_management_toggle(self): + """ + Toggle Loudness Management. + + Only available if using Telnet. + """ + if self._loudness_management: + await self.async_loudness_management_off() + else: + await self.async_loudness_management_on() + + async def async_dialog_enhancer(self, level: DialogEnhancerLevels) -> None: + """Set Dialog Enhancer level.""" + if level not in self._dialog_enhancer_levels: + raise AvrCommandError(f"{level} is not a valid dialog enhancer level") + + level_mapped = DIALOG_ENHANCER_LEVEL_MAP[level] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_dialog_enhancer.format( + level=level_mapped + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_dialog_enhancer.format(level=level_mapped) + ) + + async def async_auromatic_3d_preset(self, preset: AuroMatic3DPresets) -> None: + """Set Auro-Matic 3D Preset.""" + if preset not in self._auromatic_3d_presets: + raise AvrCommandError(f"{preset} is not a valid Auro-Matic 3D Preset") + + local_preset = AURO_MATIC_3D_PRESET_MAP[preset] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_auromatic_3d_preset.format( + preset=local_preset + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_auromatic_3d_preset.format( + preset=local_preset + ) + ) + + async def async_auromatic_3d_strength_up(self) -> None: + """Increase Auro-Matic 3D Strength.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_auromatic_3d_strength.format( + value="UP" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_auromatic_3d_strength.format(value="UP") + ) + + async def async_auromatic_3d_strength_down(self) -> None: + """Decrease Auro-Matic 3D Strength.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_auromatic_3d_strength.format( + value="DOWN" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_auromatic_3d_strength.format(value="DOWN") + ) + + async def async_auromatic_3d_strength(self, strength: int) -> None: + """ + Set Auro-Matic 3D Strength. + + :param strength: Strength value to set. Valid values are 1-16. + """ + if strength < 1 or strength > 16: + raise AvrCommandError(f"{strength} is not a valid Auro-Matic 3D Strength") + + local_strength = f"{strength:02}" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_auromatic_3d_strength.format( + value=local_strength + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_auromatic_3d_strength.format( + value=local_strength + ) + ) + + async def async_auro_3d_mode(self, mode: Auro3DModes) -> None: + """Set Auro 3D Mode.""" + if mode not in self._auro_3d_modes: + raise AvrCommandError(f"{mode} is not a valid Auro 3D Mode") + + local_mode = AURO_3D_MODE_MAP[mode] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_auro_3d_mode.format( + mode=local_mode + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_auro_3d_mode.format(mode=local_mode) + ) + + async def async_dialog_control_up(self) -> None: + """Increase Dialog Control level.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_dialog_control.format(value="UP") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_dialog_control.format(value="UP") + ) + + async def async_dialog_control_down(self) -> None: + """Decrease Dialog Control level.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_dialog_control.format(value="DOWN") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_dialog_control.format(value="DOWN") + ) + + async def async_dialog_control(self, level: int) -> None: + """ + Set Dialog Control level. + + :param level: Level to set. Valid values are 0-6. + """ + if level < 0 or level > 6: + raise AvrCommandError(f"{level} is not a valid dialog control level") + + local_level = f"{level:02}" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_dialog_control.format( + value=local_level + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_dialog_control.format(value=local_level) + ) + + async def async_speaker_virtualizer_on(self) -> None: + """Set Speaker Virtualizer to ON.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_speaker_virtualizer.format( + mode="ON" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_speaker_virtualizer.format(mode="ON") + ) + + async def async_speaker_virtualizer_off(self) -> None: + """Set Speaker Virtualizer to OFF.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_speaker_virtualizer.format( + mode="OFF" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_speaker_virtualizer.format(mode="OFF") + ) + + async def async_speaker_virtualizer_toggle(self) -> None: + """ + Toggle Speaker Virtualizer. + + Only available if using Telnet. + """ + if self._speaker_virtualizer: + await self.async_speaker_virtualizer_off() + else: + await self.async_speaker_virtualizer_on() + + async def async_effect_speaker_selection(self, mode: EffectSpeakers) -> None: + """Set Effect Speaker.""" + if mode not in self._effect_speakers: + raise AvrCommandError(f"{mode} is not a valid effect speaker selection") + + local_mode = EFFECT_SPEAKER_SELECTION_MAP[mode] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_effect_speaker_selection.format( + mode=local_mode + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_effect_speaker_selection.format( + mode=local_mode + ) + ) + + async def async_effect_speaker_selection_toggle(self) -> None: + """ + Toggle Effect Speaker Selection. + + Only available if using Telnet. + """ + if self._effect_speaker_selection == "Floor": + await self.async_effect_speaker_selection("Height + Floor") + else: + await self.async_effect_speaker_selection("Floor") + + async def async_drc(self, mode: DRCs) -> None: + """Set DRC mode.""" + if mode not in self._drcs: + raise AvrCommandError(f"{mode} is not a valid DRC mode") + + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_drc.format(mode=mode) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_drc.format(mode=mode) + ) + def sound_mode_factory(instance: DenonAVRFoundation) -> DenonAVRSoundMode: """Create DenonAVRSoundMode at receiver instances.""" diff --git a/denonavr/volume.py b/denonavr/volume.py index 720b55f..1306461 100644 --- a/denonavr/volume.py +++ b/denonavr/volume.py @@ -9,14 +9,26 @@ import logging from collections.abc import Hashable -from typing import Optional, Union +from typing import Dict, Optional, Union, get_args import attr from .appcommand import AppCommands -from .const import DENON_ATTR_SETATTR, MAIN_ZONE, STATE_ON +from .const import ( + CHANNEL_MAP, + CHANNEL_MAP_LABELS, + CHANNEL_VOLUME_MAP, + CHANNEL_VOLUME_MAP_LABELS, + DENON_ATTR_SETATTR, + MAIN_ZONE, + STATE_ON, + SUBWOOFERS_MAP, + SUBWOOFERS_MAP_LABELS, + Channels, + Subwoofers, +) from .exceptions import AvrCommandError, AvrProcessingError -from .foundation import DenonAVRFoundation +from .foundation import DenonAVRFoundation, convert_on_off_bool _LOGGER = logging.getLogger(__name__) @@ -43,7 +55,17 @@ class DenonAVRVolume(DenonAVRFoundation): _muted: Optional[bool] = attr.ib( converter=attr.converters.optional(convert_muted), default=None ) - + _channel_volumes: Optional[Dict[Channels, float]] = attr.ib(default=None) + _valid_channels = get_args(Channels) + _subwoofer: Optional[bool] = attr.ib( + converter=attr.converters.optional(convert_on_off_bool), default=None + ) + _subwoofer_levels: Optional[Dict[Subwoofers, float]] = attr.ib(default=None) + _valid_subwoofers = get_args(Subwoofers) + _lfe: Optional[int] = attr.ib(converter=attr.converters.optional(int), default=None) + _bass_sync: Optional[int] = attr.ib( + converter=attr.converters.optional(int), default=None + ) # Update tags for attributes # AppCommand.xml interface appcommand_attrs = { @@ -61,6 +83,17 @@ def setup(self) -> None: self._device.telnet_api.register_callback("MV", self._async_volume_callback) self._device.telnet_api.register_callback("MU", self._async_mute_callback) + self._device.telnet_api.register_callback( + "CV", self._async_channel_volume_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_subwoofer_state_callback + ) + self._device.telnet_api.register_callback( + "PS", self._async_subwoofer_levels_callback + ) + self._device.telnet_api.register_callback("PS", self._async_lfe_callback) + self._device.telnet_api.register_callback("PS", self._async_bass_sync_callback) self._is_setup = True @@ -85,6 +118,68 @@ async def _async_mute_callback(self, zone: str, event: str, parameter: str) -> N self._muted = parameter + async def _async_channel_volume_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a channel volume change event.""" + if event != "CV": + return + + channel_volume = parameter.split() + if len(channel_volume) != 2 or channel_volume[0] not in CHANNEL_MAP_LABELS: + return + + if self._channel_volumes is None: + self._channel_volumes = {} + + channel = CHANNEL_MAP_LABELS[channel_volume[0]] + volume = channel_volume[1] + self._channel_volumes[channel] = CHANNEL_VOLUME_MAP[volume] + + async def _async_subwoofer_state_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a subwoofer state change event.""" + if parameter[:3] == "SWR": + self._subwoofer = parameter[4:] + + async def _async_subwoofer_levels_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a subwoofer levels change event.""" + if parameter[:3] != "SWL": + return + + subwoofer_volume = parameter.split() + if ( + len(subwoofer_volume) != 2 + or subwoofer_volume[0] not in SUBWOOFERS_MAP_LABELS + ): + return + + if self._subwoofer_levels is None: + self._subwoofer_levels = {} + + subwoofer = SUBWOOFERS_MAP_LABELS[subwoofer_volume[0]] + level = subwoofer_volume[1] + self._subwoofer_levels[subwoofer] = CHANNEL_VOLUME_MAP[level] + + async def _async_lfe_callback(self, zone: str, event: str, parameter: str) -> None: + """Handle a LFE change event.""" + if parameter[:3] != "LFE": + return + + self._lfe = int(parameter[4:]) * -1 + + async def _async_bass_sync_callback( + self, zone: str, event: str, parameter: str + ) -> None: + """Handle a LFE change event.""" + if parameter[:3] != "BSC": + return + + self._bass_sync = int(parameter[4:]) * -1 + async def async_update( self, global_update: bool = False, cache_id: Optional[Hashable] = None ) -> None: @@ -141,6 +236,76 @@ def volume(self) -> Optional[float]: """ return self._volume + @property + def channel_volumes(self) -> Optional[Dict[Channels, float]]: + """ + Return the channel levels of the device in dB. + + Only available if using Telnet. + """ + return self._channel_volumes + + @property + def subwoofer(self) -> Optional[bool]: + """ + Return the state of the subwoofer. + + Only available if using Telnet. + """ + return self._subwoofer + + @property + def subwoofer_levels(self) -> Optional[Dict[Subwoofers, float]]: + """ + Return the subwoofer levels of the device in dB. + + Only available if using Telnet. + """ + return self._subwoofer_levels + + @property + def lfe(self) -> Optional[int]: + """ + Return LFE level in dB. + + Only available if using Telnet. + """ + return self._lfe + + @property + def bass_sync(self) -> Optional[int]: + """ + Return Bass Sync level in dB. + + Only available if using Telnet. + """ + return self._bass_sync + + ########## + # Getter # + ########## + def channel_volume(self, channel: Channels) -> Optional[float]: + """ + Return the volume of a channel in dB. + + Only available if using Telnet. + """ + self._is_valid_channel(channel) + if self._channel_volumes is None: + return None + return self._channel_volumes[channel] + + def subwoofer_level(self, subwoofer: Subwoofers) -> Optional[float]: + """ + Return the volume of a subwoofer in dB. + + Only available if using Telnet. + """ + self._is_valid_subwoofer(subwoofer) + if self._subwoofer_levels is None: + return None + return self._subwoofer_levels[subwoofer] + ########## # Setter # ########## @@ -210,6 +375,240 @@ async def async_mute(self, mute: bool) -> None: self._device.urls.command_mute_off ) + async def async_channel_volume_up(self, channel: Channels) -> None: + """Increase Channel volume on receiver via HTTP get command.""" + self._is_valid_channel(channel) + + mapped_channel = CHANNEL_MAP[channel] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_channel_volume.format( + channel=mapped_channel, value="UP" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_channel_volume.format( + channel=mapped_channel, value="UP" + ) + ) + + async def async_channel_volume_down(self, channel: Channels) -> None: + """Decrease Channel volume on receiver via HTTP get command.""" + self._is_valid_channel(channel) + + mapped_channel = CHANNEL_MAP[channel] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_channel_volume.format( + channel=mapped_channel, value="DOWN" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_channel_volume.format( + channel=mapped_channel, value="DOWN" + ) + ) + + async def async_channel_volume(self, channel: Channels, volume: float) -> None: + """ + Set Channel volume on receiver via HTTP get command. + + :param channel: Channel to set. + :param volume: Volume to set. Valid values are -12 to 12 with 0.5 steps. + """ + self._is_valid_channel(channel) + if volume not in CHANNEL_VOLUME_MAP_LABELS: + raise AvrCommandError(f"Invalid channel volume: {volume}") + + mapped_channel = CHANNEL_MAP[channel] + mapped_volume = CHANNEL_VOLUME_MAP_LABELS[volume] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_channel_volume.format( + channel=mapped_channel, value=mapped_volume + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_channel_volume.format( + channel=mapped_channel, value=mapped_volume + ) + ) + + async def async_channel_volumes_reset(self) -> None: + """Reset all channel volumes on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_channel_volumes_reset + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_channel_volumes_reset + ) + + async def async_subwoofer_on(self) -> None: + """Turn on Subwoofer on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_subwoofer_on_off.format(mode="ON") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_subwoofer_on_off.format(mode="ON") + ) + + async def async_subwoofer_off(self) -> None: + """Turn off Subwoofer on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_subwoofer_on_off.format(mode="OFF") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_subwoofer_on_off.format(mode="OFF") + ) + + async def async_subwoofer_toggle(self) -> None: + """ + Toggle Subwoofer on receiver via HTTP get command. + + Only available if using Telnet. + """ + if self._subwoofer: + await self.async_subwoofer_off() + else: + await self.async_subwoofer_on() + + async def async_subwoofer_level_up(self, subwoofer: Subwoofers) -> None: + """Increase Subwoofer level on receiver via HTTP get command.""" + self._is_valid_subwoofer(subwoofer) + mapped_subwoofer = SUBWOOFERS_MAP[subwoofer] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_subwoofer_level.format( + number=mapped_subwoofer, mode="UP" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_subwoofer_level.format( + number=mapped_subwoofer, mode="UP" + ) + ) + + async def async_subwoofer_level_down(self, subwoofer: Subwoofers) -> None: + """Decrease Subwoofer level on receiver via HTTP get command.""" + self._is_valid_subwoofer(subwoofer) + mapped_subwoofer = SUBWOOFERS_MAP[subwoofer] + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_subwoofer_level.format( + number=mapped_subwoofer, mode="DOWN" + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_subwoofer_level.format( + number=mapped_subwoofer, mode="DOWN" + ) + ) + + async def async_lfe_up(self) -> None: + """Increase LFE on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_lfe.format(mode="UP") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_lfe.format(mode="UP") + ) + + async def async_lfe_down(self) -> None: + """Decrease LFE on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_lfe.format(mode="DOWN") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_lfe.format(mode="DOWN") + ) + + async def async_lfe(self, lfe: int) -> None: + """ + Set LFE level on receiver via HTTP get command. + + :param lfe: LFE level to set. Valid values are -10 to 0. + """ + if lfe < -10 or lfe > 0: + raise AvrCommandError(f"Invalid LFE: {lfe}") + + lfe_local = str(lfe).replace("-", "").zfill(2) + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_lfe.format(mode=lfe_local) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_lfe.format(mode=lfe_local) + ) + + async def async_bass_sync_up(self) -> None: + """Increase Bass Sync on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_bass_sync.format(mode="UP") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_bass_sync.format(mode="UP") + ) + + async def async_bass_sync_down(self) -> None: + """Decrease Bass Sync on receiver via HTTP get command.""" + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_bass_sync.format(mode="DOWN") + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_bass_sync.format(mode="DOWN") + ) + + async def async_bass_sync(self, lfe: int) -> None: + """ + Set Bass Sync level on receiver via HTTP get command. + + :param lfe: Bass Sync level to set. Valid values are -10 to 0. + """ + if lfe < -10 or lfe > 0: + raise AvrCommandError(f"Invalid Bass Sync: {lfe}") + + bass_sync_local = str(lfe).replace("-", "").zfill(2) + if self._device.telnet_available: + await self._device.telnet_api.async_send_commands( + self._device.telnet_commands.command_bass_sync.format( + mode=bass_sync_local + ) + ) + else: + await self._device.api.async_get_command( + self._device.urls.command_bass_sync.format(mode=bass_sync_local) + ) + + @staticmethod + def _is_valid_channel(channel: Channels): + if channel not in DenonAVRVolume._valid_channels: + raise AvrCommandError("Invalid channel") + + @staticmethod + def _is_valid_subwoofer(subwoofer: Subwoofers): + if subwoofer not in DenonAVRVolume._valid_subwoofers: + raise AvrCommandError("Invalid subwoofer") + def volume_factory(instance: DenonAVRFoundation) -> DenonAVRVolume: """Create DenonAVRVolume at receiver instances.""" diff --git a/tests/test_const.py b/tests/test_const.py new file mode 100644 index 0000000..6b3daf0 --- /dev/null +++ b/tests/test_const.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This module covers some tests for mappings in const.py. + +:copyright: (c) 2016 by Oliver Goetz. +:license: MIT, see LICENSE for more details. +""" + +import unittest +from typing import get_args + +from denonavr.const import ( + CHANNEL_MAP, + DIMMER_MODE_MAP, + DIRAC_FILTER_MAP, + ECO_MODE_MAP, + HDMI_OUTPUT_MAP, + Channels, + DimmerModes, + DiracFilters, + EcoModes, + HDMIOutputs, +) + + +class TestMappings(unittest.TestCase): + """Test case for mappings in const.py.""" + + def test_dimmer_modes_mappings(self): + """Test that all dimmer modes are in the mapping.""" + for mode in get_args(DimmerModes): + self.assertIn(mode, DIMMER_MODE_MAP) + + def test_eco_modes_mappings(self): + """Test that all eco-modes are in the mapping.""" + for mode in get_args(EcoModes): + self.assertIn(mode, ECO_MODE_MAP) + + def test_hdmi_outputs_mappings(self): + """Test that all hdmi outputs are in the mapping.""" + for hdmi_output in get_args(HDMIOutputs): + self.assertIn(hdmi_output, HDMI_OUTPUT_MAP) + + def test_channel_mappings(self): + """Test that all channels are in the mapping.""" + for channel in get_args(Channels): + self.assertIn(channel, CHANNEL_MAP) + + def test_dirac_filter_mappings(self): + """Test that dirac filters are in the mapping.""" + for dirac_filter in get_args(DiracFilters): + self.assertIn(dirac_filter, DIRAC_FILTER_MAP) + + +if __name__ == "__main__": + unittest.main() From 53508abd274b7089374472e0c34cfb0478fa3b42 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Thu, 1 May 2025 17:42:46 +0200 Subject: [PATCH 08/13] Include AVC-A10H in integration tests (#329) * Include AVC-A10H in integration tests * Include A-series in the X series regex as it uses port 8080 * Fix filename * Add missing files --- denonavr/const.py | 2 +- tests/test_denonavr.py | 1 + tests/xml/AVC-A10H-AppCommand-setup-8080.xml | 6 + tests/xml/AVC-A10H-AppCommand-update-8080.xml | 237 ++ ...-A10H-AppCommand-update-soundmode-8080.xml | 6 + ...10H-AppCommand-update-tonecontrol-8080.xml | 11 + tests/xml/AVC-A10H-Deviceinfo-8080.xml | 3474 +++++++++++++++++ 7 files changed, 3736 insertions(+), 1 deletion(-) create mode 100644 tests/xml/AVC-A10H-AppCommand-setup-8080.xml create mode 100644 tests/xml/AVC-A10H-AppCommand-update-8080.xml create mode 100644 tests/xml/AVC-A10H-AppCommand-update-soundmode-8080.xml create mode 100644 tests/xml/AVC-A10H-AppCommand-update-tonecontrol-8080.xml create mode 100644 tests/xml/AVC-A10H-Deviceinfo-8080.xml diff --git a/denonavr/const.py b/denonavr/const.py index aaeff5d..07ca0ce 100644 --- a/denonavr/const.py +++ b/denonavr/const.py @@ -210,7 +210,7 @@ # AVR-X search patterns DEVICEINFO_AVR_X_PATTERN = re.compile( - r"(.*AV(C|R)-(X|S).*|.*SR500[6-9]|.*SR60(07|08|09|10|11|12|13)|." + r"(.*AV(C|R)-(X|S|A).*|.*SR500[6-9]|.*SR60(07|08|09|10|11|12|13)|." r"*SR70(07|08|09|10|11|12|13)|.*SR501[3-4]|.*NR1604|.*NR1710)" ) diff --git a/tests/test_denonavr.py b/tests/test_denonavr.py index d234976..4b62cc1 100644 --- a/tests/test_denonavr.py +++ b/tests/test_denonavr.py @@ -42,6 +42,7 @@ "AVR-3312": (NO_ZONES, denonavr.const.AVR), "NR1609": (ZONE2, denonavr.const.AVR_X_2016), "AVC-8500H": (ZONE2_ZONE3, denonavr.const.AVR_X_2016), + "AVC-A10H": (ZONE2_ZONE3, denonavr.const.AVR_X_2016), "AVR-X4300H": (ZONE2_ZONE3, denonavr.const.AVR_X_2016), "AVR-X1100W": (ZONE2, denonavr.const.AVR_X), "SR6012": (ZONE2, denonavr.const.AVR_X_2016), diff --git a/tests/xml/AVC-A10H-AppCommand-setup-8080.xml b/tests/xml/AVC-A10H-AppCommand-setup-8080.xml new file mode 100644 index 0000000..09d5d77 --- /dev/null +++ b/tests/xml/AVC-A10H-AppCommand-setup-8080.xml @@ -0,0 +1,6 @@ + + + +Denon AVC-A10H + + diff --git a/tests/xml/AVC-A10H-AppCommand-update-8080.xml b/tests/xml/AVC-A10H-AppCommand-update-8080.xml new file mode 100644 index 0000000..44e8b5b --- /dev/null +++ b/tests/xml/AVC-A10H-AppCommand-update-8080.xml @@ -0,0 +1,237 @@ + + + +ON +OFF +OFF + + + +MPLAY + + +SOURCE + + +SOURCE + + + + + +SOURCE +Source + + +CBL/SAT +TV + + +Media Player +Apple TV 4K + + +Blu-ray +Oppo Blu-ray + + +GAME1 +PC + + +GAME2 +Nintendo Switch + + +AUX1 +AUX1 + + +AUX2 +PS5 + + +AUX3 + + + +AUX4 + + + +AUX5 + + + +AUX6 + + + +AUX7 + + + +TV AUDIO +TV Audio + + +CD +CD + + +PHONO +Phono + + +TUNER +Tuner + + +NETWORK +HEOS Music + + +Bluetooth +Bluetooth + + + + + + +SOURCE +SOURCE +0 + + +CBL/SAT +CBL/SAT +1 + + +Media Player +Media Player +1 + + +Blu-ray +Blu-ray +1 + + +GAME1 +GAME1 +1 + + +GAME2 +GAME2 +1 + + +AUX1 +AUX1 +0 + + +AUX2 +AUX2 +1 + + +AUX3 +AUX3 +0 + + +AUX4 +AUX4 +0 + + +AUX5 +AUX5 +0 + + +AUX6 +AUX6 +0 + + +AUX7 +AUX7 +0 + + +TV AUDIO +TV AUDIO +0 + + +CD +CD +0 + + +PHONO +PHONO +0 + + +TUNER +TUNER +0 + + +NETWORK +NETWORK +1 + + +Bluetooth +Bluetooth +1 + + + + +Stereo + + +0 + + + + + + + + +-45.0 +variable +-20.0 +RELATIVE +-45.0dB + + +-40 +variable +-10.0 +RELATIVE +-40dB + + +-40 +variable +-10.0 +RELATIVE +-40dB + + + +off +off +off + + diff --git a/tests/xml/AVC-A10H-AppCommand-update-soundmode-8080.xml b/tests/xml/AVC-A10H-AppCommand-update-soundmode-8080.xml new file mode 100644 index 0000000..8aa7103 --- /dev/null +++ b/tests/xml/AVC-A10H-AppCommand-update-soundmode-8080.xml @@ -0,0 +1,6 @@ + + + +DTS Neo:X Cinema + + \ No newline at end of file diff --git a/tests/xml/AVC-A10H-AppCommand-update-tonecontrol-8080.xml b/tests/xml/AVC-A10H-AppCommand-update-tonecontrol-8080.xml new file mode 100644 index 0000000..a9e3710 --- /dev/null +++ b/tests/xml/AVC-A10H-AppCommand-update-tonecontrol-8080.xml @@ -0,0 +1,11 @@ + + + +0 + + + + + + + \ No newline at end of file diff --git a/tests/xml/AVC-A10H-Deviceinfo-8080.xml b/tests/xml/AVC-A10H-Deviceinfo-8080.xml new file mode 100644 index 0000000..1570256 --- /dev/null +++ b/tests/xml/AVC-A10H-Deviceinfo-8080.xml @@ -0,0 +1,3474 @@ + + +0001 +0301 +0002 +0 +01 +AV RECEIVER +AVC-A10H +02 +AVC-A10H +0123456789AB +00 +0 +3 + + + +0 +Audio +Audio +263 + +1 +SubwooferLevel + + +1 +BassSync + + +1 +SurroundParameter + + +1 +Restorer + + +1 +Audyssey + + +1 +GraphicEQ + + + +0 +Video +Video +264 + +1 +HdmiSetup + + +1 +OutputSettings + + + +0 +Inputs +Inputs +265 + +1 +SourceRename + + +1 +HideSources + + +1 +InputSelect + + + +0 +Speakers +Speakers +266 + +1 +SpeakerAB + + + +0 +Network +Network +267 + +1 +NetworkInfo + + + +0 +General +General +268 + +1 +ECO + + +1 +BTTX + + +1 +Zone2Setup + + +1 +Zone3Setup + + +1 +ZoneRename + + +1 +QuickSelectName + + +1 +FrontDisplay + + +1 +FirmwareUpdate + + +1 +SetupLock + + + +0 +SetupAssistant +Setup Assistant +269 + + + + + +English +German +French +Italian +Spanish +Dutch +Swedish +Russian +Polish + + + +0 +NetLink + + +0 +ClockAdjust + + +1 +SleepTimer +120 +10 + +1 + + + +0 +WakeupTimer + + +0 +PartyMode +01 + + +0 +BatteryMode + + +0 +DeviceColor + + + +1 +ECO + + +Main + +0 +15 +30 +60 + + + +Zone2 + +0 +120 +240 +480 + + + +Zone3 + +0 +120 +240 +480 + + + + +1 +1 +1 +1 +1 +1 +1 + + + +1 +Bluetooth Transmitter +BTTX + +1 +Transmitter +Transmitter + + +Off +0 + + +On +1 + + + + +1 +Output Mode +OutputMode + + +Bluetooth Only +1 + + +Bluetooth + Speakers +2 + + + + +1 +Connection +Connection + + +Disconnect +0 + + +Reconnect +1 + + + + +1 +1 +1 + + + +1 + +ToneControlSet_AVR + + +Tone Control +0 +12 +6 +1 +0 +12 +6 +1 + + +1 +1 + + + +1 +Subwoofer Level Adjust +SubwooferLevel +4 + +Subwoofer1 +Subwoofer2 +Subwoofer3 +Subwoofer4 + +0 +48 +24 +0.5 + +1 +1 + + + +1 +Bass Sync +BassSync +0 +16 +0 +1 + +1 +1 + + + +1 +Channel Level +ChannelLevel + + +C +Center +0 +48 +24 +0.5 + + +SW +Subwoofer 1 +0 +48 +24 +0.5 + + +SW2 +Subwoofer 2 +0 +48 +24 +0.5 + + +SW3 +Subwoofer 3 +0 +48 +24 +0.5 + + +SW4 +Subwoofer 4 +0 +48 +24 +0.5 + + +FL +Front L +0 +48 +24 +0.5 + + +FR +Front R +0 +48 +24 +0.5 + + +SL +Surround L +0 +48 +24 +0.5 + + +SR +Surround R +0 +48 +24 +0.5 + + +SB +Surround Back +0 +48 +24 +0.5 + + +SBL +Surround Back L +0 +48 +24 +0.5 + + +SBR +Surround Back R +0 +48 +24 +0.5 + + +FHL +Front Height L +0 +48 +24 +0.5 + + +FHR +Front Height R +0 +48 +24 +0.5 + + +FWL +Front Wide L +0 +48 +24 +0.5 + + +FWR +Front Wide R +0 +48 +24 +0.5 + + +TFL +Top Front L +0 +48 +24 +0.5 + + +TFR +Top Front R +0 +48 +24 +0.5 + + +TML +Top Middle L +0 +48 +24 +0.5 + + +TMR +Top Middle R +0 +48 +24 +0.5 + + +TRL +Top Rear L +0 +48 +24 +0.5 + + +TRR +Top Rear R +0 +48 +24 +0.5 + + +RHL +Rear Height L +0 +48 +24 +0.5 + + +RHR +Rear Height R +0 +48 +24 +0.5 + + +SHL +Surround Height L +0 +48 +24 +0.5 + + +SHR +Surround Height R +0 +48 +24 +0.5 + + +FDL +Front Dolby L +0 +48 +24 +0.5 + + +FDR +Front Dolby R +0 +48 +24 +0.5 + + +SDL +Surround Dolby L +0 +48 +24 +0.5 + + +SDR +Surround Dolby R +0 +48 +24 +0.5 + + +BDL +Back Dolby L +0 +48 +24 +0.5 + + +BDR +Back Dolby R +0 +48 +24 +0.5 + + +TS +Top Surround +0 +48 +24 +0.5 + + +CH +Center Height +0 +48 +24 +0.5 + + +ZRL +Reset +24 +24 +24 +24 + + + +1 +1 + + + +1 +All Zone Stereo +AllZoneStereo + +STEREO +MULTI CH STEREO + + +1 +1 +1 + + + +1 +UserManualViewer +UserManualViewer + + +1 +Front Display +FrontDisplay + +1 +Brightness + + +Bright +3 + + +Dim +2 + + +Dark +1 + + +Off +0 + + + +1 +1 + + + +1 +Channel Indicators + + +Output +1 + + +Input +2 + + + +1 +1 + + + + +1 +Information +NetworkInfo + +1 + + +1 + + + +1 +AirPlay +AirPlay + + +Off +0 + + +On +1 + + + +1 +1 + + + +1 +Spotify Connect +Spotify + + +Off +0 + + +On +1 + + + +1 +1 + + + +1 +HEOS app +HEOSapp + + +Off +0 + + +On +1 + + + +1 + + + +1 +Video Select +VideoSelect + + +Off +0 + + +On +1 + + + +1 +1 + + + +1 +Zone Rename +ZoneRename + + +Main +MAIN ZONE + + +Zone2 +ZONE2 + + +Zone3 +ZONE3 + + + +1 +1 +1 + + +1 + + + +1 +Restorer +Restorer + + +High +3 + + +Medium +2 + + +Low +1 + + +Off +0 + + + +1 +1 + + + +1 +HDMI Setup +HdmiSetup +0 + +1 +HDMI Audio Out +HdmiAudioOut + + +AVR +1 + + +TV +2 + + + + +1 +1 + + + +1 +Output Settings +OutputSettings + +1 +HDMI Video Output +HdmiVideoOut + + +Auto(Dual) +3 + + +TV 1 +1 + + +TV 2 +2 + + + + +1 +Video Mode +VideoMode + + +Auto +1 + + +Game +2 + + +Movie +3 + + +Bypass +4 + + + + +1 +HDMI Upscaler +HDMIUpscaler + + +Off +0 + + +Auto +1 + + + + +1 +1 + + + +1 +Graphic EQ +GraphicEQ + + +Off +0 + + +On +1 + + + +1 +Speaker Selection +SpeakerSelection + + +Left/Right +1 + + +Each +2 + + +All +3 + + + + +1 +Speaker Selection All +SpeakerSelectionAll + + +All +1 + + + + +1 +Speaker Selection LR +SpeakerSelectionLR + + +Front L/R +1 + + +Center +2 + + +Surround L/R +3 + + +Surround Back L/R +4 + + +Surround Back +16 + + +Front Wide L/R +5 + + +Front Height L/R +6 + + +Top Front L/R +7 + + +Top Middle L/R +8 + + +Top Rear L/R +9 + + +Surround Height L/R +10 + + +Center Height +17 + + +Top Surround +11 + + +Rear Height L/R +12 + + +Front Dolby L/R +13 + + +Surround Dolby L/R +14 + + +Back Dolby L/R +15 + + + + +1 +Speaker Selection Each +SpeakerSelectionEach + + +Front L +1 + + +Front R +2 + + +Center +3 + + +Surround L +4 + + +Surround R +5 + + +Surround Back L +6 + + +Surround Back R +7 + + +Surround Back +8 + + +Front Wide L +9 + + +Front Wide R +10 + + +Front Height L +11 + + +Front Height R +12 + + +Top Front L +13 + + +Top Front R +14 + + +Top Middle L +15 + + +Top Middle R +16 + + +Top Rear L +17 + + +Top Rear R +18 + + +Surround Height L +19 + + +Surround Height R +20 + + +Center Height +30 + + +Top Surround +21 + + +Rear Height L +22 + + +Rear Height R +25 + + +Front Dolby L +23 + + +Front Dolby R +24 + + +Surround Dolby L +26 + + +Surround Dolby R +27 + + +Back Dolby L +28 + + +Back Dolby R +29 + + + + +1 +EQ Band +EQBand + + +63 Hz +1 + + +125 Hz +2 + + +250 Hz +3 + + +500 Hz +4 + + +1 kHz +5 + + +2 kHz +6 + + +4 kHz +7 + + +8 kHz +8 + + +16 kHz +9 + + + + +1 +EQAdjustDB +EQAdjustDB +-20.0 +6.0 +0.5 + + +1 +1 +1 +1 +1 +1 +1 + + + +1 +Audyssey +Audyssey + +1 +MultEQ XT32 +MultEq + + +Reference +3 + + +L/R Bypass +2 + + +Flat +1 + + +Off +0 + + + + +1 +Dynamic EQ +DynamicEq + + +Off +0 + + +On +1 + + + + +1 +Reference Level Offset +RefLevOffset + + +0dB +0 + + ++5dB +1 + + ++10dB +2 + + ++15dB +3 + + + + +1 +Dynamic Volume +DynamicVolume + + +Off +0 + + +Light +1 + + +Medium +2 + + +Heavy +3 + + + + +1 +Audyssey LFC +AudysseyLfc + + +Off +0 + + +On +1 + + + + +1 +Containment Amount +ContainmentAmount +1 +7 +4 +1 + + +1 +1 +1 + + + +1 +Surround Parameter +SurroundParameter + +1 +Cinema EQ +CinemaEq + + +Off +0 + + +On +1 + + + + +1 +Loudness Management +Loudness + + +Off +0 + + +On +1 + + + + +1 +Dynamic Compression +DynamicCompression + + +Off +0 + + +Low +1 + + +Medium +2 + + +High +3 + + +Auto +4 + + + + +1 +Dialog Control +DialogControl +0 +6 +0 +1 + + +1 +Low Frequency Effects +LFE + +-10 +0 +0 +1 + + +0 +15 +15 +5 + + + +1 +Speaker Virtualizer +SpeakerVirtualizer + + +Off +0 + + +On +1 + + + + +1 +Center Spread +CenterSpread + + +Off +0 + + +On +1 + + + + +1 +DTS Neural:X +DTSNeuralX + + +Off +0 + + +On +1 + + + + +1 +IMAX +IMAX + + +Off +0 + + +On +1 + + +Auto +2 + + + + +1 +IMAX Audio Settings +IMAXAudioSettings + + +Auto +1 + + +Manual +2 + + + + +1 +High Pass Filter +HighPassFilter +40 +60 +80 +90 +100 +110 +120 +150 +180 +200 +250 + + +1 +Low Pass Filter +LowPassFilter +80 +90 +100 +110 +120 +150 +180 +200 +250 + + +1 +Subwoofer Output +SubwooferOutput + + +LFE +0 + + +LFE+Main +1 + + + + +1 +Auro-Matic Preset +AuroMatic3DPreset + + +Small +1 + + +Medium +2 + + +Large +3 + + +Movie +5 + + +Speech +4 + + + + +1 +Auro-Matic Strength +AuroMatic3DStrength +0 +15 +12 +1 + + +1 +Auro-3D Mode +Auro3DMode + + +Channel Expansion +1 + + +Direct +2 + + + + +1 +Delay Time +DelayTime +0 +300 +30 +3 +10 + + +1 +Effect Level +EffectLevel +1 +15 +10 +1 + + +1 +Room Size +RoomSize + + +Small +1 + + +Medium small +2 + + +Medium +3 + + +Medium large +4 + + +Large +5 + + + + +1 +Speaker Select +SpeakerSelect + + +Floor +1 + + +Floor&Height +2 + + + + +1 +Subwoofer +Subwoofer + + +Off +0 + + +On +1 + + + + +1 +1 +1 +1 + + + +1 +Audio Delay +AudioDelay + +1 +Auto Lip Sync +AutoLipSync + + +Off +0 + + +On +1 + + + + +1 +Adjust +Adjust +0 +500 +1 + + +1 +1 + + + +1 +External Contol +ExternalContol + +1 +1 + + + +1 +Dialog Enhancer +DialogEnhancer + + +High +3 + + +Medium +2 + + +Low +1 + + +Off +0 + + + +1 +1 + + + +1 +Source Rename +SourceRename +16 + +1 +1 +1 +1 +1 + + + +1 +Hide Sources +HideSources + + +Show +1 + + +Hide +0 + + + +1 +1 + + + +1 +Input Select +InputSelect + +1 +Input Mode +InputMode + + +No Input +0 + + +Auto +1 + + +HDMI +2 + + +Digital +3 + + +Analog +4 + + +7.1CH IN +5 + + +ARC +6 + + +eARC +7 + + + + +1 +1 + + + +1 +Zone2 Setup +Zone2Setup + +1 +Bass +Bass +-10 +10 +1 + + +1 +Treble +Treble +-10 +10 +1 + + +1 +Lch Level +LchLevel +-12 +12 +1 + + +1 +Rch Level +RchLevel +-12 +12 +1 + + +1 +1 +1 +1 + + + +1 +Zone3 Setup +Zone3Setup + +1 +Bass +Bass +-10 +10 +1 + + +1 +Treble +Treble +-10 +10 +1 + + +1 +Lch Level +LchLevel +-12 +12 +1 + + +1 +Rch Level +RchLevel +-12 +12 +1 + + +1 +1 +1 +1 + + + +1 +Firmware +FirmwareUpdate + +1 +Check for Update +Update + + +1 +Auto-Update +AutoUpdate + + +1 +Allow Update +AllowUpdate + + +1 +1 +1 +1 +1 + + + +1 +Setup Lock +SetupLock + +1 +Lock +Lock + + +Off +0 + + +On +1 + + + + +1 +1 + + + +1 +Sound Mode +SoundMode + + + +MOVIE +1 + + +MUSIC +2 + + +GAME +3 + + +PURE +4 + + + + +1 +1 +1 +1 + + + +1 +INFO +StatusInfo + +1 +1 +1 +1 +1 +1 + + + +1 +Front Speaker +SpeakerAB + + +Speaker A +1 + + +Speaker B +2 + + +Speaker A+B +3 + + + +1 + + + +1 +Auto SetupMenu Off +AutoSetupMenuOff + + +1 +Speaker Preset +SpeakerPreset + + +Preset 1 +1 + + +Preset 2 +2 + + + +1 +1 + + + +1 +Dirac Live +DiracLive + + +Off +0 + + +Slot 1 +1 + + +Slot 2 +2 + + +Slot 3 +3 + + + +1 +1 + + + +1 +Menu Lock +MenuLock + + +Off +0 + + +On +1 + + + +1 + + + + + +0 +Clock + + +1 +AllZonePower + + +1 +AllZoneMute + + +0 +SystemFavorites +50 + +0 + + + + + + +0 + + +1 + + +1 +240 +Dialog Enhancer +Dialog Enhancer + + +1 +210 +Restorer +Restorer + + +1 +204 +Sleep Timer +SleepTimer + + +2 +110 +Cursor +Cursor + + +3 +111 +Quick Select 1 +Quick Select1 + + +3 +111 +Quick Select 2 +Quick Select2 + + +3 +111 +Quick Select 3 +Quick Select3 + + +3 +111 +Quick Select 4 +Quick Select4 + + +4 +4 +CBL/SAT +CBL/SAT + + +4 +1 +Blu-ray +Blu-ray + + +4 +8 +Game1 +GAME1 + + +4 +8 +Game2 +GAME2 + + +4 +46 +AUX1 +AUX1 + + +4 +46 +AUX2 +AUX2 + + +4 +46 +Media Player +Media Player + + +4 +17 +CD +CD + + +4 +19 +Tuner +TUNER + + +4 +3 +TV Audio +TV AUDIO + + +4 +59 +Bluetooth +Bluetooth + + +4 +16 +Phono +PHONO + + + + +1 + + +1 +98.0 +0.5 + + +OFF +OFF +98.0 + + +86.0 ++6.0dB +86.0 + + +83.0 ++3.0dB +83.0 + + +80.0 +0.0dB +80.0 + + +74.0 +-6.0dB +74.0 + + +68.0 +-12.0dB +68.0 + + +60.0 +-20.0dB +60.0 + + +50.0 +-30.0dB +50.0 + + +40.0 +-40.0dB +40.0 + + +-6.0dB + +1 + + + +1 + + +1 + + +4 +CBL/SAT +CBL/SAT +root/CBLSAT +3 + + + +46 +Media Player +Media Player +root/Media Player +3 + + + +1 +Blu-ray +Blu-ray +root/Blu-ray +3 +BdOperation + + +8 +GAME1 +Game1 +root/GAME1 +3 + + + +8 +GAME2 +Game2 +root/GAME2 +3 + + + +46 +AUX1 +AUX1 +root/AUX1 +3 + + + +46 +AUX2 +AUX2 +root/AUX2 +3 + + + +46 +AUX3 +AUX3 +root/AUX3 +3 + + + +46 +AUX4 +AUX4 +root/AUX4 +3 + + + +46 +AUX5 +AUX5 +root/AUX5 +3 + + + +46 +AUX6 +AUX6 +root/AUX6 +3 + + + +46 +AUX7 +AUX7 +root/AUX7 +3 + + + +3 +TV AUDIO +TV Audio +root/TV AUDIO +3 + + + +17 +CD +CD +root/CD +3 + + + +16 +PHONO +Phono +root/PHONO +3 + + + +19 +TUNER +Tuner +root/TUNER +3 +TunerOperation + + +23 +NETWORK +HEOS Music +root/NETWORK +3 +NetUsb + + +59 +Bluetooth +Bluetooth +root/Bluetooth +3 + + + + +1 +1 +1 + + + + +1 +Restorer +Restorer + + +217 +High +MODE1 + + +218 +Medium +MODE2 + + +219 +Low +MODE3 + + +216 +OFF +OFF + + + +1 + + + +0 +ToneControl + + + + +1 +Cursor +1 + + +1 +4 + +Quick Select 1 +Quick Select1 +1 + + +Quick Select 2 +Quick Select2 +2 + + +Quick Select 3 +Quick Select3 +3 + + +Quick Select 4 +Quick Select4 +4 + + +1 +1 +1 +1 +1 + + + +1 +Blu-ray + +1 + + + +0 +CD + +0 + + + +0 + + + + + +1 + + +1 + + +1 +240 +Dialog Enhancer +Dialog Enhancer + + +1 +210 +Restorer +Restorer + + +1 +204 +Sleep Timer +SleepTimer + + +2 +110 +Cursor +Cursor + + +3 +111 +Quick Select 1 +Quick Select1 + + +3 +111 +Quick Select 2 +Quick Select2 + + +3 +111 +Quick Select 3 +Quick Select3 + + +3 +111 +Quick Select 4 +Quick Select4 + + +4 +4 +CBL/SAT +CBL/SAT + + +4 +1 +Blu-ray +Blu-ray + + +4 +8 +Game1 +GAME1 + + +4 +8 +Game2 +GAME2 + + +4 +46 +AUX1 +AUX1 + + +4 +46 +AUX2 +AUX2 + + +4 +46 +Media Player +Media Player + + +4 +17 +CD +CD + + +4 +19 +Tuner +TUNER + + +4 +3 +TV Audio +TV AUDIO + + +4 +59 +Bluetooth +Bluetooth + + +4 +16 +Phono +PHONO + + + + +1 + + +1 +98.0 +0.5 + + +OFF +OFF +98.0 + + +86.0 ++6.0dB +86.0 + + +83.0 ++3.0dB +83.0 + + +80.0 +0.0dB +80.0 + + +74.0 +-6.0dB +74.0 + + +68.0 +-12.0dB +68.0 + + +60.0 +-20.0dB +60.0 + + +50.0 +-30.0dB +50.0 + + +40.0 +-40.0dB +40.0 + + +-6.0dB + +1 + + + +1 + + +1 + + +0 +SOURCE +SOURCE +root/SOURCE +3 + + + +4 +CBL/SAT +CBL/SAT +root/CBLSAT +3 + + + +46 +Media Player +Media Player +root/Media Player +3 + + + +1 +Blu-ray +Blu-ray +root/Blu-ray +3 +BdOperation + + +8 +GAME1 +Game1 +root/GAME1 +3 + + + +8 +GAME2 +Game2 +root/GAME2 +3 + + + +46 +AUX1 +AUX1 +root/AUX1 +3 + + + +46 +AUX2 +AUX2 +root/AUX2 +3 + + + +46 +AUX3 +AUX3 +root/AUX3 +3 + + + +46 +AUX4 +AUX4 +root/AUX4 +3 + + + +46 +AUX5 +AUX5 +root/AUX5 +3 + + + +46 +AUX6 +AUX6 +root/AUX6 +3 + + + +46 +AUX7 +AUX7 +root/AUX7 +3 + + + +3 +TV AUDIO +TV Audio +root/TV AUDIO +3 + + + +17 +CD +CD +root/CD +3 + + + +16 +PHONO +Phono +root/PHONO +3 + + + +19 +TUNER +Tuner +root/TUNER +3 +TunerOperation + + +23 +NETWORK +HEOS Music +root/NETWORK +3 +NetUsb + + +59 +Bluetooth +Bluetooth +root/Bluetooth +3 + + + + +1 +1 +1 + + + + +0 +Restorer +Restorer + + +217 +High +MODE1 + + +218 +Medium +MODE2 + + +219 +Low +MODE3 + + +216 +OFF +OFF + + + +1 + + + +0 +ToneControl + + + + +1 +Cursor +1 + + +1 +4 + +Quick Select 1 +Quick Select1 +1 + + +Quick Select 2 +Quick Select2 +2 + + +Quick Select 3 +Quick Select3 +3 + + +Quick Select 4 +Quick Select4 +4 + + +1 +1 + + + +1 +Blu-ray + +1 + + + +0 +CD + +0 + + + +0 + + + + + +2 + + +1 + + +1 +240 +Dialog Enhancer +Dialog Enhancer + + +1 +210 +Restorer +Restorer + + +1 +204 +Sleep Timer +SleepTimer + + +2 +110 +Cursor +Cursor + + +3 +111 +Quick Select 1 +Quick Select1 + + +3 +111 +Quick Select 2 +Quick Select2 + + +3 +111 +Quick Select 3 +Quick Select3 + + +3 +111 +Quick Select 4 +Quick Select4 + + +4 +4 +CBL/SAT +CBL/SAT + + +4 +1 +Blu-ray +Blu-ray + + +4 +8 +Game1 +GAME1 + + +4 +8 +Game2 +GAME2 + + +4 +46 +AUX1 +AUX1 + + +4 +46 +AUX2 +AUX2 + + +4 +46 +Media Player +Media Player + + +4 +17 +CD +CD + + +4 +19 +Tuner +TUNER + + +4 +3 +TV Audio +TV AUDIO + + +4 +59 +Bluetooth +Bluetooth + + +4 +16 +Phono +PHONO + + + + +1 + + +1 +98.0 +0.5 + + +OFF +OFF +98.0 + + +86.0 ++6.0dB +86.0 + + +83.0 ++3.0dB +83.0 + + +80.0 +0.0dB +80.0 + + +74.0 +-6.0dB +74.0 + + +68.0 +-12.0dB +68.0 + + +60.0 +-20.0dB +60.0 + + +50.0 +-30.0dB +50.0 + + +40.0 +-40.0dB +40.0 + + +-6.0dB + +1 + + + +1 + + +1 + + +0 +SOURCE +SOURCE +root/SOURCE +3 + + + +4 +CBL/SAT +CBL/SAT +root/CBLSAT +3 + + + +46 +Media Player +Media Player +root/Media Player +3 + + + +1 +Blu-ray +Blu-ray +root/Blu-ray +3 +BdOperation + + +8 +GAME1 +Game1 +root/GAME1 +3 + + + +8 +GAME2 +Game2 +root/GAME2 +3 + + + +46 +AUX1 +AUX1 +root/AUX1 +3 + + + +46 +AUX2 +AUX2 +root/AUX2 +3 + + + +46 +AUX3 +AUX3 +root/AUX3 +3 + + + +46 +AUX4 +AUX4 +root/AUX4 +3 + + + +46 +AUX5 +AUX5 +root/AUX5 +3 + + + +46 +AUX6 +AUX6 +root/AUX6 +3 + + + +46 +AUX7 +AUX7 +root/AUX7 +3 + + + +3 +TV AUDIO +TV Audio +root/TV AUDIO +3 + + + +17 +CD +CD +root/CD +3 + + + +16 +PHONO +Phono +root/PHONO +3 + + + +19 +TUNER +Tuner +root/TUNER +3 +TunerOperation + + +23 +NETWORK +HEOS Music +root/NETWORK +3 +NetUsb + + +59 +Bluetooth +Bluetooth +root/Bluetooth +3 + + + + +1 +1 +1 + + + + +0 +Restorer +Restorer + + +217 +High +MODE1 + + +218 +Medium +MODE2 + + +219 +Low +MODE3 + + +216 +OFF +OFF + + + +1 + + + +0 +ToneControl + + + + +1 +Cursor +1 + + +1 +4 + +Quick Select 1 +Quick Select1 +1 + + +Quick Select 2 +Quick Select2 +2 + + +Quick Select 3 +Quick Select3 +3 + + +Quick Select 4 +Quick Select4 +4 + + +1 +1 + + + +1 +Blu-ray + +1 + + + +0 +CD + +0 + + + +0 + + + + From da533731726a3fcdb52db3e94ea12b09d16dd069 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Thu, 1 May 2025 17:45:07 +0200 Subject: [PATCH 09/13] Set timeout on _send_confirmation_timeout (#330) --- denonavr/foundation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/denonavr/foundation.py b/denonavr/foundation.py index 3749842..0265f48 100644 --- a/denonavr/foundation.py +++ b/denonavr/foundation.py @@ -1999,6 +1999,7 @@ def set_api_timeout( instance._device.api.timeout = value instance._device.api.read_timeout = max(value, 15.0) instance._device.telnet_api.timeout = value + instance._device.telnet_api._send_confirmation_timeout = value return value From f7bedb373d6cecb7ef52b38dc68b2cb0151f00f3 Mon Sep 17 00:00:00 2001 From: ol-iver Date: Thu, 1 May 2025 18:21:44 +0200 Subject: [PATCH 10/13] Update README.md --- README.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index e731433..b0bdea5 100644 --- a/README.md +++ b/README.md @@ -88,27 +88,11 @@ Other `async` methods available include: * `d.async_set_volume(50)` ## Collection of HTTP calls -For a collection of HTTP calls for Denon receivers please have a look at the `doc` folder. +For a collection of HTTP calls for Denon receivers please have a look at the [doc folder](doc/XML_data_dump.txt). ## License MIT -## Author -@ol-iver: https://github.com/ol-iver - -## Contributors -@soldag: https://github.com/soldag -@shapiromatron: https://github.com/shapiromatron -@glance-: https://github.com/glance- -@p3dda: https://github.com/p3dda -@russel: https://github.com/russell -@starkillerOG: https://github.com/starkillerOG -@andrewsayre: https://github.com/andrewsayre -@JPHutchins: https://github.com/JPHutchins -@MarBra: https://github.com/MarBra -@dcmeglio: https://github.com/dcmeglio -@bdraco: https://github.com/bdraco - ## Users Home Assistant: https://github.com/home-assistant/home-assistant/ denonavr-cli: https://pypi.org/project/denonavr-cli/ From 475f6963383986ac58f331ed9a07fe05716338f1 Mon Sep 17 00:00:00 2001 From: Henrik Widlund <4659350+henrikwidlund@users.noreply.github.com> Date: Sat, 3 May 2025 12:34:33 +0200 Subject: [PATCH 11/13] Correct value types based on issues after testing (#332) --- denonavr/denonavr.py | 4 +--- denonavr/foundation.py | 11 +++++++++-- denonavr/volume.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/denonavr/denonavr.py b/denonavr/denonavr.py index 53f797e..d25a4c5 100644 --- a/denonavr/denonavr.py +++ b/denonavr/denonavr.py @@ -623,13 +623,11 @@ def video_processing_mode(self) -> Optional[str]: return self._device.video_processing_mode @property - def tactile_transducer(self) -> Optional[str]: + def tactile_transducer(self) -> Optional[bool]: """ Return the tactile transducer state of the device. Only available if using Telnet. - - Possible values are ON, OFF and Number representing the intensity """ return self._device.tactile_transducer diff --git a/denonavr/foundation.py b/denonavr/foundation.py index 0265f48..e8d440b 100644 --- a/denonavr/foundation.py +++ b/denonavr/foundation.py @@ -93,6 +93,15 @@ def convert_on_off_bool(value: str) -> Optional[bool]: return None +def convert_on_off_bool_str(value: str) -> Optional[Union[bool, str]]: + """Convert a ON/OFF string to bool with fallback to raw value.""" + val = convert_on_off_bool(value) + if val is not None: + return val + + return value + + @attr.s(auto_attribs=True, on_setattr=DENON_ATTR_SETATTR) class DenonAVRDeviceInfo: """Implements a class with device information of the receiver.""" @@ -927,8 +936,6 @@ def tactile_transducer(self) -> Optional[str]: Return the tactile transducer state of the device. Only available if using Telnet. - - Possible values are ON, OFF and Number representing the intensity """ return self._tactile_transducer diff --git a/denonavr/volume.py b/denonavr/volume.py index 1306461..d21ddb1 100644 --- a/denonavr/volume.py +++ b/denonavr/volume.py @@ -60,7 +60,9 @@ class DenonAVRVolume(DenonAVRFoundation): _subwoofer: Optional[bool] = attr.ib( converter=attr.converters.optional(convert_on_off_bool), default=None ) - _subwoofer_levels: Optional[Dict[Subwoofers, float]] = attr.ib(default=None) + _subwoofer_levels: Optional[Dict[Subwoofers, Union[float, bool]]] = attr.ib( + default=None + ) _valid_subwoofers = get_args(Subwoofers) _lfe: Optional[int] = attr.ib(converter=attr.converters.optional(int), default=None) _bass_sync: Optional[int] = attr.ib( @@ -162,7 +164,11 @@ async def _async_subwoofer_levels_callback( subwoofer = SUBWOOFERS_MAP_LABELS[subwoofer_volume[0]] level = subwoofer_volume[1] - self._subwoofer_levels[subwoofer] = CHANNEL_VOLUME_MAP[level] + val = convert_on_off_bool(level) + if val is not None: + self._subwoofer_levels[subwoofer] = val + else: + self._subwoofer_levels[subwoofer] = CHANNEL_VOLUME_MAP[level] async def _async_lfe_callback(self, zone: str, event: str, parameter: str) -> None: """Handle a LFE change event.""" @@ -255,9 +261,9 @@ def subwoofer(self) -> Optional[bool]: return self._subwoofer @property - def subwoofer_levels(self) -> Optional[Dict[Subwoofers, float]]: + def subwoofer_levels(self) -> Optional[Dict[Subwoofers, Union[bool, float]]]: """ - Return the subwoofer levels of the device in dB. + Return the subwoofer levels of the device in dB or power state. Only available if using Telnet. """ From 2514a3533f4f5aaef06be40114025e9b7ddbbf65 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sat, 3 May 2025 14:49:01 +0200 Subject: [PATCH 12/13] Fix lookup maps which might cause telnet callback exceptions (#331) --- denonavr/const.py | 19 ++++++++++++++++++- denonavr/volume.py | 22 ++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/denonavr/const.py b/denonavr/const.py index 07ca0ce..b0a41a5 100644 --- a/denonavr/const.py +++ b/denonavr/const.py @@ -1350,12 +1350,29 @@ EcoModes = Literal["On", "Auto", "Off"] """Eco Modes.""" -EffectSpeakers = Literal["Floor", "Height + Floor"] +EffectSpeakers = Literal[ + "Floor", + "Front", + "Front Height", + "Front Wide", + "Front Height + Front Wide", + "Height + Floor", + "Surround Back", + "Surround Back + Front Height", + "Surround Back + Front Wide", +] """Effect Speakers.""" EFFECT_SPEAKER_SELECTION_MAP = { "Floor": "FL", + "Front": "FR", + "Front Height": "FH", + "Front Wide": "FW", + "Front Height + Front Wide": "HW", "Height + Floor": "HF", + "Surround Back": "SB", + "Surround Back + Front Height": "BH", + "Surround Back + Front Wide": "BW", } EFFECT_SPEAKER_SELECTION_MAP_LABELS = { value: key for key, value in EFFECT_SPEAKER_SELECTION_MAP.items() diff --git a/denonavr/volume.py b/denonavr/volume.py index d21ddb1..206d3f3 100644 --- a/denonavr/volume.py +++ b/denonavr/volume.py @@ -60,9 +60,8 @@ class DenonAVRVolume(DenonAVRFoundation): _subwoofer: Optional[bool] = attr.ib( converter=attr.converters.optional(convert_on_off_bool), default=None ) - _subwoofer_levels: Optional[Dict[Subwoofers, Union[float, bool]]] = attr.ib( - default=None - ) + _subwoofer_levels_adjustment: bool = attr.ib(default=True) + _subwoofer_levels: Optional[Dict[Subwoofers, float]] = attr.ib(default=None) _valid_subwoofers = get_args(Subwoofers) _lfe: Optional[int] = attr.ib(converter=attr.converters.optional(int), default=None) _bass_sync: Optional[int] = attr.ib( @@ -128,7 +127,11 @@ async def _async_channel_volume_callback( return channel_volume = parameter.split() - if len(channel_volume) != 2 or channel_volume[0] not in CHANNEL_MAP_LABELS: + if ( + len(channel_volume) != 2 + or channel_volume[0] not in CHANNEL_MAP_LABELS + or channel_volume[1] not in CHANNEL_VOLUME_MAP + ): return if self._channel_volumes is None: @@ -166,8 +169,8 @@ async def _async_subwoofer_levels_callback( level = subwoofer_volume[1] val = convert_on_off_bool(level) if val is not None: - self._subwoofer_levels[subwoofer] = val - else: + self._subwoofer_levels_adjustment = val + elif level in CHANNEL_VOLUME_MAP: self._subwoofer_levels[subwoofer] = CHANNEL_VOLUME_MAP[level] async def _async_lfe_callback(self, zone: str, event: str, parameter: str) -> None: @@ -263,11 +266,14 @@ def subwoofer(self) -> Optional[bool]: @property def subwoofer_levels(self) -> Optional[Dict[Subwoofers, Union[bool, float]]]: """ - Return the subwoofer levels of the device in dB or power state. + Return the subwoofer levels of the device in dB when enabled. Only available if using Telnet. """ - return self._subwoofer_levels + if self._subwoofer_levels_adjustment: + return self._subwoofer_levels + + return None @property def lfe(self) -> Optional[int]: From 3261b3c60038ec49fee782fd4d6f7e8a640cb5dc Mon Sep 17 00:00:00 2001 From: ol-iver Date: Sun, 4 May 2025 10:34:41 +0200 Subject: [PATCH 13/13] Update to version 1.1.0 --- denonavr/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/denonavr/__init__.py b/denonavr/__init__.py index 8b8f4e0..d9a3d6a 100644 --- a/denonavr/__init__.py +++ b/denonavr/__init__.py @@ -17,7 +17,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) __title__ = "denonavr" -__version__ = "1.0.2-dev" +__version__ = "1.1.0" async def async_discover(): diff --git a/pyproject.toml b/pyproject.toml index 770cf85..45bc4f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "denonavr" -version = "1.0.2-dev" +version = "1.1.0" authors = [{name = "Oliver Goetz", email = "scarface@mywoh.de"}] license = {text = "MIT"} description = "Automation Library for Denon AVR receivers"