mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
Add support for telnet connections for Denonavr integration (#85980)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
b4c343b1a2
commit
3d9d79684d
@ -1,21 +1,26 @@
|
|||||||
"""The denonavr component."""
|
"""The denonavr component."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from denonavr import DenonAVR
|
||||||
from denonavr.exceptions import AvrNetworkError, AvrTimoutError
|
from denonavr.exceptions import AvrNetworkError, AvrTimoutError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
|
|
||||||
from .config_flow import (
|
from .config_flow import (
|
||||||
CONF_SHOW_ALL_SOURCES,
|
CONF_SHOW_ALL_SOURCES,
|
||||||
|
CONF_UPDATE_AUDYSSEY,
|
||||||
|
CONF_USE_TELNET,
|
||||||
CONF_ZONE2,
|
CONF_ZONE2,
|
||||||
CONF_ZONE3,
|
CONF_ZONE3,
|
||||||
DEFAULT_SHOW_SOURCES,
|
DEFAULT_SHOW_SOURCES,
|
||||||
DEFAULT_TIMEOUT,
|
DEFAULT_TIMEOUT,
|
||||||
|
DEFAULT_UPDATE_AUDYSSEY,
|
||||||
|
DEFAULT_USE_TELNET,
|
||||||
DEFAULT_ZONE2,
|
DEFAULT_ZONE2,
|
||||||
DEFAULT_ZONE3,
|
DEFAULT_ZONE3,
|
||||||
DOMAIN,
|
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_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES),
|
||||||
entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
|
entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
|
||||||
entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
|
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),
|
lambda: get_async_client(hass),
|
||||||
)
|
)
|
||||||
try:
|
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)
|
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
|
return True
|
||||||
|
|
||||||
@ -66,6 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
config_entry, PLATFORMS
|
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]()
|
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||||
|
|
||||||
# Remove zone2 and zone3 entities if needed
|
# Remove zone2 and zone3 entities if needed
|
||||||
|
@ -31,12 +31,15 @@ CONF_ZONE3 = "zone3"
|
|||||||
CONF_MANUFACTURER = "manufacturer"
|
CONF_MANUFACTURER = "manufacturer"
|
||||||
CONF_SERIAL_NUMBER = "serial_number"
|
CONF_SERIAL_NUMBER = "serial_number"
|
||||||
CONF_UPDATE_AUDYSSEY = "update_audyssey"
|
CONF_UPDATE_AUDYSSEY = "update_audyssey"
|
||||||
|
CONF_USE_TELNET = "use_telnet"
|
||||||
|
|
||||||
DEFAULT_SHOW_SOURCES = False
|
DEFAULT_SHOW_SOURCES = False
|
||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
DEFAULT_ZONE2 = False
|
DEFAULT_ZONE2 = False
|
||||||
DEFAULT_ZONE3 = False
|
DEFAULT_ZONE3 = False
|
||||||
DEFAULT_UPDATE_AUDYSSEY = False
|
DEFAULT_UPDATE_AUDYSSEY = False
|
||||||
|
DEFAULT_USE_TELNET = False
|
||||||
|
DEFAULT_USE_TELNET_NEW_INSTALL = True
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
|
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
|
||||||
|
|
||||||
@ -77,6 +80,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY
|
CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY
|
||||||
),
|
),
|
||||||
): bool,
|
): 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.show_all_sources = DEFAULT_SHOW_SOURCES
|
||||||
self.zone2 = DEFAULT_ZONE2
|
self.zone2 = DEFAULT_ZONE2
|
||||||
self.zone3 = DEFAULT_ZONE3
|
self.zone3 = DEFAULT_ZONE3
|
||||||
|
self.use_telnet = DEFAULT_USE_TELNET_NEW_INSTALL
|
||||||
self.d_receivers: list[dict[str, Any]] = []
|
self.d_receivers: list[dict[str, Any]] = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -176,7 +186,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self.show_all_sources,
|
self.show_all_sources,
|
||||||
self.zone2,
|
self.zone2,
|
||||||
self.zone3,
|
self.zone3,
|
||||||
lambda: get_async_client(self.hass),
|
use_telnet=False,
|
||||||
|
update_audyssey=False,
|
||||||
|
async_client_getter=lambda: get_async_client(self.hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -216,6 +228,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_MANUFACTURER: receiver.manufacturer,
|
CONF_MANUFACTURER: receiver.manufacturer,
|
||||||
CONF_SERIAL_NUMBER: self.serial_number,
|
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:
|
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
"codeowners": ["@ol-iver", "@starkillerOG"],
|
"codeowners": ["@ol-iver", "@starkillerOG"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_push",
|
||||||
"loggers": ["denonavr"],
|
"loggers": ["denonavr"],
|
||||||
"requirements": ["denonavr==0.10.12"],
|
"requirements": ["denonavr==0.11.1"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Denon",
|
"manufacturer": "Denon",
|
||||||
|
@ -247,12 +247,47 @@ class DenonDevice(MediaPlayerEntity):
|
|||||||
and MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
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_log_errors
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest status information from device."""
|
"""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:
|
if self._update_audyssey:
|
||||||
await self._receiver.async_update_audyssey()
|
await receiver.async_update_audyssey()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> MediaPlayerState | None:
|
def state(self) -> MediaPlayerState | None:
|
||||||
|
@ -19,6 +19,8 @@ class ConnectDenonAVR:
|
|||||||
show_all_inputs: bool,
|
show_all_inputs: bool,
|
||||||
zone2: bool,
|
zone2: bool,
|
||||||
zone3: bool,
|
zone3: bool,
|
||||||
|
use_telnet: bool,
|
||||||
|
update_audyssey: bool,
|
||||||
async_client_getter: Callable,
|
async_client_getter: Callable,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the class."""
|
"""Initialize the class."""
|
||||||
@ -27,6 +29,8 @@ class ConnectDenonAVR:
|
|||||||
self._host = host
|
self._host = host
|
||||||
self._show_all_inputs = show_all_inputs
|
self._show_all_inputs = show_all_inputs
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
self._use_telnet = use_telnet
|
||||||
|
self._update_audyssey = update_audyssey
|
||||||
|
|
||||||
self._zones: dict[str, str | None] = {}
|
self._zones: dict[str, str | None] = {}
|
||||||
if zone2:
|
if zone2:
|
||||||
@ -85,5 +89,11 @@ class ConnectDenonAVR:
|
|||||||
# Use httpx.AsyncClient getter provided by Home Assistant
|
# Use httpx.AsyncClient getter provided by Home Assistant
|
||||||
receiver.set_async_client_getter(self._async_client_getter)
|
receiver.set_async_client_getter(self._async_client_getter)
|
||||||
await receiver.async_setup()
|
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
|
self._receiver = receiver
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"flow_title": "{name}",
|
"flow_title": "{name}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"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": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::ip%]"
|
"host": "[%key:common::config_flow::data::ip%]"
|
||||||
},
|
},
|
||||||
@ -40,7 +41,8 @@
|
|||||||
"show_all_sources": "Show all sources",
|
"show_all_sources": "Show all sources",
|
||||||
"zone2": "Set up Zone 2",
|
"zone2": "Set up Zone 2",
|
||||||
"zone3": "Set up Zone 3",
|
"zone3": "Set up Zone 3",
|
||||||
"update_audyssey": "Update Audyssey settings"
|
"update_audyssey": "Update Audyssey settings",
|
||||||
|
"use_telnet": "Use Telnet connection"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -997,7 +997,7 @@
|
|||||||
"denonavr": {
|
"denonavr": {
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_push",
|
||||||
"name": "Denon AVR Network Receivers"
|
"name": "Denon AVR Network Receivers"
|
||||||
},
|
},
|
||||||
"heos": {
|
"heos": {
|
||||||
|
@ -586,7 +586,7 @@ deluge-client==1.7.1
|
|||||||
demetriek==0.4.0
|
demetriek==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.denonavr
|
# homeassistant.components.denonavr
|
||||||
denonavr==0.10.12
|
denonavr==0.11.1
|
||||||
|
|
||||||
# homeassistant.components.devolo_home_control
|
# homeassistant.components.devolo_home_control
|
||||||
devolo-home-control-api==0.18.2
|
devolo-home-control-api==0.18.2
|
||||||
|
@ -463,7 +463,7 @@ deluge-client==1.7.1
|
|||||||
demetriek==0.4.0
|
demetriek==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.denonavr
|
# homeassistant.components.denonavr
|
||||||
denonavr==0.10.12
|
denonavr==0.11.1
|
||||||
|
|
||||||
# homeassistant.components.devolo_home_control
|
# homeassistant.components.devolo_home_control
|
||||||
devolo-home-control-api==0.18.2
|
devolo-home-control-api==0.18.2
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.components.denonavr.config_flow import (
|
|||||||
CONF_SHOW_ALL_SOURCES,
|
CONF_SHOW_ALL_SOURCES,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
CONF_UPDATE_AUDYSSEY,
|
CONF_UPDATE_AUDYSSEY,
|
||||||
|
CONF_USE_TELNET,
|
||||||
CONF_ZONE2,
|
CONF_ZONE2,
|
||||||
CONF_ZONE3,
|
CONF_ZONE3,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -96,6 +97,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None:
|
|||||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||||
}
|
}
|
||||||
|
assert result["options"] == {CONF_USE_TELNET: True}
|
||||||
|
|
||||||
|
|
||||||
async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> None:
|
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_MANUFACTURER: TEST_MANUFACTURER,
|
||||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||||
}
|
}
|
||||||
|
assert result["options"] == {CONF_USE_TELNET: True}
|
||||||
|
|
||||||
|
|
||||||
async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> None:
|
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_MANUFACTURER: TEST_MANUFACTURER,
|
||||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||||
}
|
}
|
||||||
|
assert result["options"] == {CONF_USE_TELNET: True}
|
||||||
|
|
||||||
|
|
||||||
async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None:
|
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_MANUFACTURER: TEST_MANUFACTURER,
|
||||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||||
}
|
}
|
||||||
|
assert result["options"] == {CONF_USE_TELNET: True}
|
||||||
|
|
||||||
|
|
||||||
async def test_config_flow_ssdp_not_denon(hass: HomeAssistant) -> None:
|
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 = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
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
|
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_ZONE2: True,
|
||||||
CONF_ZONE3: True,
|
CONF_ZONE3: True,
|
||||||
CONF_UPDATE_AUDYSSEY: False,
|
CONF_UPDATE_AUDYSSEY: False,
|
||||||
|
CONF_USE_TELNET: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user