Add support for telnet connections for Denonavr integration (#85980)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
dcmeglio 2023-02-13 19:36:09 -05:00 committed by GitHub
parent b4c343b1a2
commit 3d9d79684d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 105 additions and 12 deletions

View file

@ -1,21 +1,26 @@
"""The denonavr component."""
import logging
from denonavr import DenonAVR
from denonavr.exceptions import AvrNetworkError, AvrTimoutError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.httpx_client import get_async_client
from .config_flow import (
CONF_SHOW_ALL_SOURCES,
CONF_UPDATE_AUDYSSEY,
CONF_USE_TELNET,
CONF_ZONE2,
CONF_ZONE3,
DEFAULT_SHOW_SOURCES,
DEFAULT_TIMEOUT,
DEFAULT_UPDATE_AUDYSSEY,
DEFAULT_USE_TELNET,
DEFAULT_ZONE2,
DEFAULT_ZONE3,
DOMAIN,
@ -40,6 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.options.get(CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES),
entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
entry.options.get(CONF_USE_TELNET, DEFAULT_USE_TELNET),
entry.options.get(CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY),
lambda: get_async_client(hass),
)
try:
@ -56,6 +63,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
use_telnet = entry.options.get(CONF_USE_TELNET, DEFAULT_USE_TELNET)
async def _async_disconnect(event: Event) -> None:
"""Disconnect from Telnet."""
if use_telnet and receiver is not None:
await receiver.async_telnet_disconnect()
if use_telnet:
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect)
)
return True
@ -66,6 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, PLATFORMS
)
if config_entry.options.get(CONF_USE_TELNET, DEFAULT_USE_TELNET):
receiver: DenonAVR = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER]
await receiver.async_telnet_disconnect()
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
# Remove zone2 and zone3 entities if needed

View file

@ -31,12 +31,15 @@ CONF_ZONE3 = "zone3"
CONF_MANUFACTURER = "manufacturer"
CONF_SERIAL_NUMBER = "serial_number"
CONF_UPDATE_AUDYSSEY = "update_audyssey"
CONF_USE_TELNET = "use_telnet"
DEFAULT_SHOW_SOURCES = False
DEFAULT_TIMEOUT = 5
DEFAULT_ZONE2 = False
DEFAULT_ZONE3 = False
DEFAULT_UPDATE_AUDYSSEY = False
DEFAULT_USE_TELNET = False
DEFAULT_USE_TELNET_NEW_INSTALL = True
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
@ -77,6 +80,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY
),
): bool,
vol.Optional(
CONF_USE_TELNET,
default=self.config_entry.options.get(
CONF_USE_TELNET, DEFAULT_USE_TELNET
),
): bool,
}
)
@ -97,6 +106,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.show_all_sources = DEFAULT_SHOW_SOURCES
self.zone2 = DEFAULT_ZONE2
self.zone3 = DEFAULT_ZONE3
self.use_telnet = DEFAULT_USE_TELNET_NEW_INSTALL
self.d_receivers: list[dict[str, Any]] = []
@staticmethod
@ -176,7 +186,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.show_all_sources,
self.zone2,
self.zone3,
lambda: get_async_client(self.hass),
use_telnet=False,
update_audyssey=False,
async_client_getter=lambda: get_async_client(self.hass),
)
try:
@ -216,6 +228,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_MANUFACTURER: receiver.manufacturer,
CONF_SERIAL_NUMBER: self.serial_number,
},
options={CONF_USE_TELNET: DEFAULT_USE_TELNET_NEW_INSTALL},
)
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:

View file

@ -4,9 +4,9 @@
"codeowners": ["@ol-iver", "@starkillerOG"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_polling",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==0.10.12"],
"requirements": ["denonavr==0.11.1"],
"ssdp": [
{
"manufacturer": "Denon",

View file

@ -247,12 +247,47 @@ class DenonDevice(MediaPlayerEntity):
and MediaPlayerEntityFeature.SELECT_SOUND_MODE
)
self._receiver.register_callback("ALL", self._telnet_callback)
self._telnet_was_healthy: bool | None = None
async def _telnet_callback(self, zone, event, parameter):
"""Process a telnet command callback."""
if zone != self._receiver.zone:
return
self.async_write_ha_state()
async def async_will_remove_from_hass(self) -> None:
"""Clean up the entity."""
self._receiver.unregister_callback("ALL", self._telnet_callback)
@async_log_errors
async def async_update(self) -> None:
"""Get the latest status information from device."""
await self._receiver.async_update()
receiver = self._receiver
# We can only skip the update if telnet was healthy after
# the last update and is still healthy now to ensure that
# we don't miss any state changes while telnet is down
# or reconnecting.
if (
telnet_is_healthy := receiver.telnet_connected and receiver.telnet_healthy
) and self._telnet_was_healthy:
await receiver.input.async_update_media_state()
return
# if async_update raises an exception, we don't want to skip the next update
# so we set _telnet_was_healthy to None here and only set it to the value
# before the update if the update was successful
self._telnet_was_healthy = None
await receiver.async_update()
self._telnet_was_healthy = telnet_is_healthy
if self._update_audyssey:
await self._receiver.async_update_audyssey()
await receiver.async_update_audyssey()
@property
def state(self) -> MediaPlayerState | None:

View file

@ -19,6 +19,8 @@ class ConnectDenonAVR:
show_all_inputs: bool,
zone2: bool,
zone3: bool,
use_telnet: bool,
update_audyssey: bool,
async_client_getter: Callable,
) -> None:
"""Initialize the class."""
@ -27,6 +29,8 @@ class ConnectDenonAVR:
self._host = host
self._show_all_inputs = show_all_inputs
self._timeout = timeout
self._use_telnet = use_telnet
self._update_audyssey = update_audyssey
self._zones: dict[str, str | None] = {}
if zone2:
@ -85,5 +89,11 @@ class ConnectDenonAVR:
# Use httpx.AsyncClient getter provided by Home Assistant
receiver.set_async_client_getter(self._async_client_getter)
await receiver.async_setup()
# Do an initial update if telnet is used.
if self._use_telnet:
await receiver.async_update()
if self._update_audyssey:
await receiver.async_update_audyssey()
await receiver.async_telnet_connect()
self._receiver = receiver

View file

@ -3,6 +3,7 @@
"flow_title": "{name}",
"step": {
"user": {
"description": "By default, this integration uses a Telnet connection to your receiver to receive real-time updates. Only one Telnet connection to your receiver can be established at a time. The Telnet connection can be disabled after setting up the integration.",
"data": {
"host": "[%key:common::config_flow::data::ip%]"
},
@ -40,7 +41,8 @@
"show_all_sources": "Show all sources",
"zone2": "Set up Zone 2",
"zone3": "Set up Zone 3",
"update_audyssey": "Update Audyssey settings"
"update_audyssey": "Update Audyssey settings",
"use_telnet": "Use Telnet connection"
}
}
}

View file

@ -997,7 +997,7 @@
"denonavr": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling",
"iot_class": "local_push",
"name": "Denon AVR Network Receivers"
},
"heos": {

View file

@ -586,7 +586,7 @@ deluge-client==1.7.1
demetriek==0.4.0
# homeassistant.components.denonavr
denonavr==0.10.12
denonavr==0.11.1
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.2

View file

@ -463,7 +463,7 @@ deluge-client==1.7.1
demetriek==0.4.0
# homeassistant.components.denonavr
denonavr==0.10.12
denonavr==0.11.1
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.2

View file

@ -11,6 +11,7 @@ from homeassistant.components.denonavr.config_flow import (
CONF_SHOW_ALL_SOURCES,
CONF_TYPE,
CONF_UPDATE_AUDYSSEY,
CONF_USE_TELNET,
CONF_ZONE2,
CONF_ZONE3,
DOMAIN,
@ -96,6 +97,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None:
CONF_MANUFACTURER: TEST_MANUFACTURER,
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
}
assert result["options"] == {CONF_USE_TELNET: True}
async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> None:
@ -129,6 +131,7 @@ async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> Non
CONF_MANUFACTURER: TEST_MANUFACTURER,
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
}
assert result["options"] == {CONF_USE_TELNET: True}
async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> None:
@ -171,6 +174,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non
CONF_MANUFACTURER: TEST_MANUFACTURER,
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
}
assert result["options"] == {CONF_USE_TELNET: True}
async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None:
@ -322,6 +326,7 @@ async def test_config_flow_ssdp(hass: HomeAssistant) -> None:
CONF_MANUFACTURER: TEST_MANUFACTURER,
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
}
assert result["options"] == {CONF_USE_TELNET: True}
async def test_config_flow_ssdp_not_denon(hass: HomeAssistant) -> None:
@ -421,7 +426,12 @@ async def test_options_flow(hass: HomeAssistant) -> None:
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True},
user_input={
CONF_SHOW_ALL_SOURCES: True,
CONF_ZONE2: True,
CONF_ZONE3: True,
CONF_USE_TELNET: False,
},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
@ -430,6 +440,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
CONF_ZONE2: True,
CONF_ZONE3: True,
CONF_UPDATE_AUDYSSEY: False,
CONF_USE_TELNET: False,
}