diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index 5d5b86a..345b8c4 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 = device.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) - if not self._use_telnet: + 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: self._expected_volume = volume - self.events.emit(Events.UPDATE, self.id, {MediaAttr.VOLUME: volume}) @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,16 @@ 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()) 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()) 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..141601a 100644 --- a/intg-denonavr/setup_flow.py +++ b/intg-denonavr/setup_flow.py @@ -226,6 +226,16 @@ async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | }, "field": {"checkbox": {"value": True}}, }, + { + "id": "volume_step", + "label": { + "en": "Volume step", + "fr": "Pallier de volume", + }, + "field": { + "number": {"value": 0.5, "min": 0.5, "max": 10, "steps": 1, "decimals": 1, "unit": {"en": "dB"}} + }, + }, { "id": "info", "label": {"en": "Please note:", "de": "Bitte beachten:", "fr": "Veuillez noter:"}, @@ -268,6 +278,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 > 10: + 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 +323,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()