Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a configurable volume step (in setup flow) and improved refresh of volume #38

Merged
merged 8 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading