From 0a6d811e7d840008dbf288e391bdc3fd06034826 Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Fri, 3 May 2024 09:47:57 +0200 Subject: [PATCH 1/8] Improved volume refresh and added configurable volume step --- intg-denonavr/avr.py | 50 ++++++++++++++++++++++++++----------- intg-denonavr/config.py | 26 ++++++++++++++++--- intg-denonavr/setup_flow.py | 16 ++++++++++++ 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index 5d5b86a..2fbcc58 100644 --- a/intg-denonavr/avr.py +++ b/intg-denonavr/avr.py @@ -8,7 +8,7 @@ import asyncio import logging import time -from asyncio import AbstractEventLoop +from asyncio import AbstractEventLoop, Lock from enum import IntEnum from functools import wraps from typing import Any, Awaitable, Callable, Concatenate, Coroutine, ParamSpec, TypeVar @@ -200,7 +200,8 @@ def __init__( self.id: str = device.id # friendly name from configuration self._name: str = device.name - self.events = AsyncIOEventEmitter(loop or asyncio.get_running_loop()) + self._event_loop = loop or asyncio.get_running_loop() + self.events = AsyncIOEventEmitter(self._event_loop) self._zones: dict[str, str | None] = {} if device.zone2: self._zones["Zone2"] = None @@ -221,10 +222,11 @@ def __init__( self._connecting: bool = False self._connection_attempts: int = 0 self._reconnect_delay: float = MIN_RECONNECT_DELAY - self._getting_data: bool = False # Workaround for weird state behaviour. Sometimes "off" is always returned from the denonlib! self._expected_state: States = States.UNKNOWN + self._volume_step = VOLUME_STEP + self._update_lock = Lock() _LOG.debug("Denon AVR created: %s", device.address) @@ -515,10 +517,10 @@ async def async_update_receiver_data(self): - an async_update task is still running. - a (re-)connection task is currently running. """ - if self._getting_data or not self._active or self._connecting: + if self._update_lock.locked() or not self._active or self._connecting: return - self._getting_data = True + await self._update_lock.acquire() try: receiver = self._receiver @@ -549,7 +551,7 @@ async def async_update_receiver_data(self): self._notify_updated_data() finally: - self._getting_data = False + self._update_lock.release() def _notify_updated_data(self): """Notify listeners that the AVR data has been updated.""" @@ -661,21 +663,31 @@ async def set_volume_level(self, volume: float | None) -> ucapi.StatusCodes: if volume_denon > 18: volume_denon = float(18) await self._receiver.async_set_volume(volume_denon) + self._expected_volume = volume if not self._use_telnet: - self._expected_volume = volume self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: volume}) + else: + await self.async_update_receiver_data() @async_handle_denonlib_errors async def volume_up(self) -> ucapi.StatusCodes: """Send volume-up command to AVR.""" - await self._receiver.async_volume_up() - self._increase_expected_volume() + if self._use_telnet and self._expected_volume is not None and self._volume_step != 0.5: + self._expected_volume = min(self._expected_volume + self._volume_step, 100) + await self.set_volume_level(self._expected_volume) + else: + await self._receiver.async_volume_up() + self._increase_expected_volume() @async_handle_denonlib_errors async def volume_down(self) -> ucapi.StatusCodes: """Send volume-down command to AVR.""" - await self._receiver.async_volume_down() - self._decrease_expected_volume() + if self._use_telnet and self._expected_volume is not None and self._volume_step != 0.5: + self._expected_volume = max(self._expected_volume - self._volume_step, 0) + await self.set_volume_level(self._expected_volume) + else: + await self._receiver.async_volume_down() + self._decrease_expected_volume() @async_handle_denonlib_errors async def play_pause(self) -> ucapi.StatusCodes: @@ -699,6 +711,8 @@ async def mute(self, muted: bool) -> ucapi.StatusCodes: await self._receiver.async_mute(muted) if not self._use_telnet: self.events.emit(Events.UPDATE, self.id, {MediaAttr.MUTED: muted}) + else: + await self.async_update_receiver_data() @async_handle_denonlib_errors async def select_source(self, source: str | None) -> ucapi.StatusCodes: @@ -823,12 +837,18 @@ def _increase_expected_volume(self): """Without telnet, increase expected volume and send update event.""" if not self._use_telnet or self._expected_volume is None: return - self._expected_volume = min(self._expected_volume + VOLUME_STEP, 100) - self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: self._expected_volume}) + self._expected_volume = min(self._expected_volume + self._volume_step, 100) + # Send updated volume if no update task in progress + if not self._update_lock.locked(): + self._event_loop.create_task(self._receiver.async_update()) + # self.events.emit(Events.UPDATE,self.id, {MediaAttr.VOLUME: self._expected_volume}) def _decrease_expected_volume(self): """Without telnet, decrease expected volume and send update event.""" if not self._use_telnet or self._expected_volume is None: return - self._expected_volume = max(self._expected_volume - VOLUME_STEP, 0) - self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: self._expected_volume}) + self._expected_volume = max(self._expected_volume - self._volume_step, 0) + # Send updated volume if no update task in progress + if not self._update_lock.locked(): + self._event_loop.create_task(self._receiver.async_update()) + # self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: self._expected_volume}) diff --git a/intg-denonavr/config.py b/intg-denonavr/config.py index 86de8ec..f474560 100644 --- a/intg-denonavr/config.py +++ b/intg-denonavr/config.py @@ -49,6 +49,7 @@ class AvrDevice: update_audyssey: bool zone2: bool zone3: bool + volume_step: float class _EnhancedJSONEncoder(json.JSONEncoder): @@ -114,6 +115,13 @@ def update(self, atv: AvrDevice) -> bool: if item.id == atv.id: item.address = atv.address item.name = atv.name + item.support_sound_mode = atv.support_sound_mode + item.show_all_inputs = atv.show_all_inputs + item.use_telnet = atv.use_telnet + item.update_audyssey = atv.update_audyssey + item.zone2 = atv.zone2 + item.zone3 = atv.zone3 + item.volume_step = atv.volume_step return self.store() return False @@ -166,10 +174,20 @@ def load(self) -> bool: with open(self._cfg_file_path, "r", encoding="utf-8") as f: data = json.load(f) for item in data: - try: - self._config.append(AvrDevice(**item)) - except TypeError as ex: - _LOG.warning("Invalid configuration entry will be ignored: %s", ex) + # not using AvrDevice(**item) to be able to migrate old configuration files with missing attributes + atv = AvrDevice( + item.get("id"), + item.get("name"), + item.get("address"), + item.get("support_sound_mode", True), + item.get("show_all_inputs", False), + item.get("use_telnet", True), + item.get("update_audyssey", False), + item.get("zone2", False), + item.get("zone3", False), + item.get("volume_step", 0.5), + ) + self._config.append(atv) return True except OSError: _LOG.error("Cannot open the config file") diff --git a/intg-denonavr/setup_flow.py b/intg-denonavr/setup_flow.py index b7c6b2c..336039b 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -226,6 +226,14 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | }, "field": {"checkbox": {"value": True}}, }, + { + "id": "volume_step", + "label": { + "en": "Volume step", + "fr": "Pallier de volume", + }, + "field": {"text": {"value": "0.5"}}, + }, { "id": "info", "label": {"en": "Please note:", "de": "Bitte beachten:", "fr": "Veuillez noter:"}, @@ -268,6 +276,13 @@ async def handle_device_choice(msg: UserDataResponse) -> SetupComplete | SetupEr zone2 = msg.input_values.get("zone2") == "true" zone3 = msg.input_values.get("zone3") == "true" use_telnet = msg.input_values.get("use_telnet") == "true" + volume_step = 0.5 + try: + volume_step = float(msg.input_values.get("volume_step", 0.5)) + if volume_step < 0.1 or volume_step > 99: + return SetupError(error_type=IntegrationSetupError.OTHER) + except ValueError: + return SetupError(error_type=IntegrationSetupError.OTHER) # Telnet connection not required for connection check and retrieving model information connect_denonavr = ConnectDenonAVR( @@ -306,6 +321,7 @@ async def handle_device_choice(msg: UserDataResponse) -> SetupComplete | SetupEr update_audyssey=update_audyssey, zone2=zone2, zone3=zone3, + volume_step=volume_step, ) config.devices.add(device) # triggers DenonAVR instance creation config.devices.store() From f0d3542287e56c038c6924a65790a426a0f5f60c Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Fri, 3 May 2024 09:52:56 +0200 Subject: [PATCH 2/8] Cleaning --- intg-denonavr/avr.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index 2fbcc58..05f02b4 100644 --- a/intg-denonavr/avr.py +++ b/intg-denonavr/avr.py @@ -841,7 +841,6 @@ def _increase_expected_volume(self): # Send updated volume if no update task in progress if not self._update_lock.locked(): self._event_loop.create_task(self._receiver.async_update()) - # self.events.emit(Events.UPDATE,self.id, {MediaAttr.VOLUME: self._expected_volume}) def _decrease_expected_volume(self): """Without telnet, decrease expected volume and send update event.""" @@ -851,4 +850,3 @@ def _decrease_expected_volume(self): # Send updated volume if no update task in progress if not self._update_lock.locked(): self._event_loop.create_task(self._receiver.async_update()) - # self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: self._expected_volume}) From f5a1d4b83f51de278b54cc69d8712c5231765da4 Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Fri, 3 May 2024 10:11:58 +0200 Subject: [PATCH 3/8] Fix --- intg-denonavr/avr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index 05f02b4..8f6a211 100644 --- a/intg-denonavr/avr.py +++ b/intg-denonavr/avr.py @@ -225,7 +225,7 @@ def __init__( # Workaround for weird state behaviour. Sometimes "off" is always returned from the denonlib! self._expected_state: States = States.UNKNOWN - self._volume_step = VOLUME_STEP + self._volume_step = device.volume_step self._update_lock = Lock() _LOG.debug("Denon AVR created: %s", device.address) From e35795416821835e77ba1134fb38b14af9f499ff Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Fri, 3 May 2024 10:34:28 +0200 Subject: [PATCH 4/8] Improved update algorithm again --- intg-denonavr/avr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index 8f6a211..345b8c4 100644 --- a/intg-denonavr/avr.py +++ b/intg-denonavr/avr.py @@ -663,11 +663,11 @@ async def set_volume_level(self, volume: float | None) -> ucapi.StatusCodes: if volume_denon > 18: volume_denon = float(18) await self._receiver.async_set_volume(volume_denon) - self._expected_volume = volume - if not self._use_telnet: - self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: volume}) + self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: volume}) + if self._use_telnet and not self._update_lock.locked(): + await self._event_loop.create_task(self.async_update_receiver_data()) else: - await self.async_update_receiver_data() + self._expected_volume = volume @async_handle_denonlib_errors async def volume_up(self) -> ucapi.StatusCodes: From 301c4d21b53c79bb923b484f8f0b3a3e080e8e08 Mon Sep 17 00:00:00 2001 From: BAIN THOUVEREZ Damien DTSI/BLI Date: Wed, 5 Jun 2024 07:59:52 +0200 Subject: [PATCH 5/8] Limited max volume step to 10 and used number field in setup flow --- intg-denonavr/setup_flow.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/intg-denonavr/setup_flow.py b/intg-denonavr/setup_flow.py index 336039b..f5530da 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -232,7 +232,16 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | "en": "Volume step", "fr": "Pallier de volume", }, - "field": {"text": {"value": "0.5"}}, + "field": { + "number": { + "value": 0.5, + "min": 0.1, + "max": 10, + "steps": 0.1, + "decimals": 1, + "unit": {"en": "dB"} + } + } }, { "id": "info", @@ -279,7 +288,7 @@ async def handle_device_choice(msg: UserDataResponse) -> SetupComplete | SetupEr volume_step = 0.5 try: volume_step = float(msg.input_values.get("volume_step", 0.5)) - if volume_step < 0.1 or volume_step > 99: + if volume_step < 0.1 or volume_step > 10: return SetupError(error_type=IntegrationSetupError.OTHER) except ValueError: return SetupError(error_type=IntegrationSetupError.OTHER) From e13d31d49a77cf6f983d985f09e4cf8cb00b9ce8 Mon Sep 17 00:00:00 2001 From: BAIN THOUVEREZ Damien DTSI/BLI Date: Wed, 5 Jun 2024 08:03:11 +0200 Subject: [PATCH 6/8] Reformatting --- intg-denonavr/setup_flow.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/intg-denonavr/setup_flow.py b/intg-denonavr/setup_flow.py index f5530da..d37cf4b 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -233,15 +233,8 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | "fr": "Pallier de volume", }, "field": { - "number": { - "value": 0.5, - "min": 0.1, - "max": 10, - "steps": 0.1, - "decimals": 1, - "unit": {"en": "dB"} - } - } + "number": {"value": 0.5, "min": 0.1, "max": 10, "steps": 0.1, "decimals": 1, "unit": {"en": "dB"}} + }, }, { "id": "info", From da49115d42eceafa4f52c5db11ae753520235b21 Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:32:02 +0200 Subject: [PATCH 7/8] Rollbacked number format in setup flow for volume step --- intg-denonavr/setup_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/intg-denonavr/setup_flow.py b/intg-denonavr/setup_flow.py index d37cf4b..0d459b0 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -232,9 +232,7 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | "en": "Volume step", "fr": "Pallier de volume", }, - "field": { - "number": {"value": 0.5, "min": 0.1, "max": 10, "steps": 0.1, "decimals": 1, "unit": {"en": "dB"}} - }, + "field": {"text": {"value": "0.5"}}, }, { "id": "info", From 7246d71e3fd7d60ab9f4e8eebbc1ed12bec47e6a Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:18:38 +0200 Subject: [PATCH 8/8] Changed step field to number format with step 1 instead of 0.5, awaiting fix on API side --- intg-denonavr/setup_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/intg-denonavr/setup_flow.py b/intg-denonavr/setup_flow.py index 0d459b0..141601a 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -232,7 +232,9 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | "en": "Volume step", "fr": "Pallier de volume", }, - "field": {"text": {"value": "0.5"}}, + "field": { + "number": {"value": 0.5, "min": 0.5, "max": 10, "steps": 1, "decimals": 1, "unit": {"en": "dB"}} + }, }, { "id": "info",