Skip to content

Commit

Permalink
feat: Added a configurable volume step (in setup flow) and improved r…
Browse files Browse the repository at this point in the history
…efresh of volume (#38)

* Improved volume refresh and added configurable volume step
* Limited max volume step to 10 and used number field in setup flow

---------

Co-authored-by: BAIN THOUVEREZ Damien DTSI/BLI <[email protected]>
  • Loading branch information
albaintor and BAIN THOUVEREZ Damien DTSI/BLI authored Jun 11, 2024
1 parent ee07cb0 commit 0f2aa35
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 20 deletions.
50 changes: 34 additions & 16 deletions intg-denonavr/avr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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())
26 changes: 22 additions & 4 deletions intg-denonavr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class AvrDevice:
update_audyssey: bool
zone2: bool
zone3: bool
volume_step: float


class _EnhancedJSONEncoder(json.JSONEncoder):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
18 changes: 18 additions & 0 deletions intg-denonavr/setup_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:"},
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 0f2aa35

Please sign in to comment.