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.

8 changes: 5 additions & 3 deletions intg-denonavr/avr.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
AvrTimoutError,
DenonAvrError,
)
from pyee import AsyncIOEventEmitter
from pyee.asyncio import AsyncIOEventEmitter
from ucapi.media_player import Attributes as MediaAttr

_LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -404,7 +404,9 @@ async def connect(self):
_LOG.info("Connecting AVR %s on %s", self.id, self._receiver.host)
self.events.emit(Events.CONNECTING, self.id)
request_start = time.time()
await self._receiver.async_setup()

if not self._receiver._is_setup:
await self._receiver.async_setup()
await self._receiver.async_update()
if self._use_telnet:
if self._update_audyssey:
Expand All @@ -429,7 +431,7 @@ async def connect(self):
self.model_name,
self.name,
self.id,
self._receiver.state,
self._receiver.state
)

self._active = True
Expand Down
175 changes: 140 additions & 35 deletions intg-denonavr/setup_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +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 +72,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,44 +110,124 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se
"""
global _setup_step

_LOG.debug("Starting driver setup")
_setup_step = SetupSteps.CONFIGURATION_MODE
# pylint: disable=line-too-long
return RequestUserInput(
{"en": "Setup mode", "de": "Setup Modus"},
[
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 and click _Next_."
"The device must be on the same network as the remote."
),
"de": (
"Leer lassen, um automatische Erkennung zu verwenden und auf _Weiter_ klicken."
"Das Gerät muss sich im gleichen Netzwerk wie die Fernbedienung befinden."
),
"fr": (
"Laissez le champ vide pour utiliser la découverte automatique et cliquez sur _Suivant_." # noqa: E501
"L'appareil doit être sur le même réseau que la télécommande"
),
}
}
"id": "add",
"label": {
"en": "Add a new device",
"de": "Neues Gerät hinzufügen",
"fr": "Ajouter un nouvel appareil",
},
},
{
"field": {"text": {"value": ""}},
"id": "address",
"label": {"en": "IP address", "de": "IP-Adresse", "fr": "Adresse IP"},
},
],
)
]

# 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
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pyee>=9.0
denonavr~=0.11.6
ucapi==0.1.7
pyee>=12.1.1
denonavr~=1.0.1
ucapi==0.2.0