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 support multiple instances of Denon AVR #35

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
33 changes: 33 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/integration-denonavr.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions .idea/misc.xml
albaintor marked this conversation as resolved.
Show resolved Hide resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions intg-denonavr/avr.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@

DISCOVERY_AFTER_CONNECTION_ERRORS = 10

AVR_COMMAND_URL="/goform/formiPhoneAppDirect.xml"


class Events(IntEnum):
"""Internal driver events."""
Expand Down Expand Up @@ -716,6 +718,65 @@ async def select_sound_mode(self, sound_mode: str | None) -> ucapi.StatusCodes:
return ucapi.StatusCodes.BAD_REQUEST
await self._receiver.async_set_sound_mode(sound_mode)

@async_handle_denonlib_errors
async def cursor_up(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNCUP")

@async_handle_denonlib_errors
async def cursor_down(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNCDN")

@async_handle_denonlib_errors
async def cursor_left(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNCLT")

@async_handle_denonlib_errors
async def cursor_right(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNCRT")

@async_handle_denonlib_errors
async def cursor_enter(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNENT")

@async_handle_denonlib_errors
async def info(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNINF")

@async_handle_denonlib_errors
async def options(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNOPT")

@async_handle_denonlib_errors
async def back(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNRTN")

@async_handle_denonlib_errors
async def setup_open(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNMEN%20ON")

@async_handle_denonlib_errors
async def setup_close(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNMEN%20OFF")

@async_handle_denonlib_errors
async def setup(self) -> ucapi.StatusCodes:
"""Send previous-track command to AVR."""
res = await self._receiver.async_get_command(AVR_COMMAND_URL+"?MNMEN?")
if res is not None and res == "MNMEN ON":
await self.setup_close()
else:
await self.setup_open()

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:
Expand Down
26 changes: 26 additions & 0 deletions intg-denonavr/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ def __init__(self, device: AvrDevice, receiver: avr.DenonDevice):
Features.MEDIA_IMAGE_URL,
Features.MEDIA_TYPE,
Features.SELECT_SOURCE,
Features.DPAD,
Features.SETTINGS,
Features.MENU,
Features.CONTEXT_MENU,
Features.INFO

]
attributes = {
Attributes.STATE: States.UNAVAILABLE,
Expand Down Expand Up @@ -112,6 +118,26 @@ async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> St
res = await self._receiver.select_source(params.get("source"))
elif cmd_id == Commands.SELECT_SOUND_MODE:
res = await self._receiver.select_sound_mode(params.get("mode"))
elif cmd_id == Commands.CURSOR_UP:
res = await self._receiver.cursor_up()
elif cmd_id == Commands.CURSOR_DOWN:
res = await self._receiver.cursor_down()
elif cmd_id == Commands.CURSOR_LEFT:
res = await self._receiver.cursor_left()
elif cmd_id == Commands.CURSOR_RIGHT:
res = await self._receiver.cursor_right()
elif cmd_id == Commands.CURSOR_ENTER:
res = await self._receiver.cursor_enter()
elif cmd_id == Commands.BACK:
res = await self._receiver.back()
elif cmd_id == Commands.SETTINGS:
res = await self._receiver.setup()
elif cmd_id == Commands.MENU:
res = await self._receiver.setup()
elif cmd_id == Commands.CONTEXT_MENU:
res = await self._receiver.options()
elif cmd_id == Commands.INFO:
res = await self._receiver.info()
else:
return StatusCodes.NOT_IMPLEMENTED

Expand Down
161 changes: 139 additions & 22 deletions intg-denonavr/setup_flow.py
albaintor marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,31 @@ class SetupSteps(IntEnum):

INIT = 0
CONFIGURATION_MODE = 1
DEVICE_CHOICE = 2
DISCOVER = 2
DEVICE_CHOICE = 3


_setup_step = SetupSteps.INIT

_cfg_add_device: bool = False
_user_input_discovery = RequestUserInput(
{"en": "Setup mode", "de": "Setup Modus"},
[
{"field": {"text": {"value": ""}}, "id": "address", "label": {"en": "IP address", "de": "IP-Adresse"}},
{
"id": "info",
"label": {"en": ""},
"field": {
"label": {
"value": {
"en": "Leave blank to use auto-discovery.",
"de": "Leer lassen, um automatische Erkennung zu verwenden.",
"fr": "Laissez le champ vide pour utiliser la découverte automatique.",
}
}
},
},
],
)

async def driver_setup_handler(msg: SetupDriver) -> SetupAction:
"""
Expand All @@ -51,14 +71,18 @@ async def driver_setup_handler(msg: SetupDriver) -> SetupAction:
:return: the setup action on how to continue
"""
global _setup_step
global _cfg_add_device

if isinstance(msg, DriverSetupRequest):
_setup_step = SetupSteps.INIT
_cfg_add_device = False
return await handle_driver_setup(msg)
if isinstance(msg, UserDataResponse):
_LOG.debug(msg)
if _setup_step == SetupSteps.CONFIGURATION_MODE and "address" in msg.input_values:
if _setup_step == SetupSteps.CONFIGURATION_MODE and "action" in msg.input_values:
return await handle_configuration_mode(msg)
if _setup_step == SetupSteps.DISCOVER and "address" in msg.input_values:
return await _handle_discovery(msg)
if _setup_step == SetupSteps.DEVICE_CHOICE and "choice" in msg.input_values:
return await handle_device_choice(msg)
_LOG.error("No or invalid user response was received: %s", msg)
Expand All @@ -85,30 +109,123 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se
"""
global _setup_step

_LOG.debug("Starting driver setup")
_setup_step = SetupSteps.CONFIGURATION_MODE
return RequestUserInput(
{"en": "Setup mode", "de": "Setup Modus"},
[
{"field": {"text": {"value": ""}}, "id": "address", "label": {"en": "IP address", "de": "IP-Adresse"}},
reconfigure = _msg.reconfigure
_LOG.debug("Starting driver setup, reconfigure=%s", reconfigure)
if reconfigure:
_setup_step = SetupSteps.CONFIGURATION_MODE

# get all configured devices for the user to choose from
dropdown_devices = []
for device in config.devices.all():
dropdown_devices.append({"id": device.id, "label": {"en": f"{device.name} ({device.id})"}})

# TODO #12 externalize language texts
# build user actions, based on available devices
dropdown_actions = [
{
"id": "info",
"label": {"en": ""},
"field": {
"label": {
"value": {
"en": "Leave blank to use auto-discovery.",
"de": "Leer lassen, um automatische Erkennung zu verwenden.",
"fr": "Laissez le champ vide pour utiliser la découverte automatique.",
}
}
"id": "add",
"label": {
"en": "Add a new device",
"de": "Neues Gerät hinzufügen",
"fr": "Ajouter un nouvel appareil",
},
},
],
)
]

# add remove & reset actions if there's at least one configured device
if dropdown_devices:
dropdown_actions.append(
{
"id": "remove",
"label": {
"en": "Delete selected device",
"de": "Selektiertes Gerät löschen",
"fr": "Supprimer l'appareil sélectionné",
},
},
)
dropdown_actions.append(
{
"id": "reset",
"label": {
"en": "Reset configuration and reconfigure",
"de": "Konfiguration zurücksetzen und neu konfigurieren",
"fr": "Réinitialiser la configuration et reconfigurer",
},
},
)
else:
# dummy entry if no devices are available
dropdown_devices.append({"id": "", "label": {"en": "---"}})

return RequestUserInput(
{"en": "Configuration mode", "de": "Konfigurations-Modus"},
[
{
"field": {"dropdown": {"value": dropdown_devices[0]["id"], "items": dropdown_devices}},
"id": "choice",
"label": {
"en": "Configured devices",
"de": "Konfigurierte Geräte",
"fr": "Appareils configurés",
},
},
{
"field": {"dropdown": {"value": dropdown_actions[0]["id"], "items": dropdown_actions}},
"id": "action",
"label": {
"en": "Action",
"de": "Aktion",
"fr": "Appareils configurés",
},
},
],
)

# Initial setup, make sure we have a clean configuration
config.devices.clear() # triggers device instance removal
_setup_step = SetupSteps.DISCOVER
return _user_input_discovery


async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | SetupComplete | SetupError:
"""
Process user data response in a setup process.

If ``address`` field is set by the user: try connecting to device and retrieve model information.
Otherwise, start Android TV discovery and present the found devices to the user to choose from.

:param msg: response data from the requested user data
:return: the setup action on how to continue
"""
global _setup_step
global _cfg_add_device

action = msg.input_values["action"]

# workaround for web-configurator not picking up first response
await asyncio.sleep(1)

async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | SetupError:
match action:
case "add":
_cfg_add_device = True
case "remove":
choice = msg.input_values["choice"]
if not config.devices.remove(choice):
_LOG.warning("Could not remove device from configuration: %s", choice)
return SetupError(error_type=IntegrationSetupError.OTHER)
config.devices.store()
return SetupComplete()
case "reset":
config.devices.clear() # triggers device instance removal
case _:
_LOG.error("Invalid configuration action: %s", action)
return SetupError(error_type=IntegrationSetupError.OTHER)

_setup_step = SetupSteps.DISCOVER
return _user_input_discovery

async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupError:
"""
Process user data response in a setup process.

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pyee>=9.0
denonavr~=0.11.4
albaintor marked this conversation as resolved.
Show resolved Hide resolved
ucapi==0.1.3
ucapi==0.1.6
albaintor marked this conversation as resolved.
Show resolved Hide resolved