From 2b6fd0df6a0ff64f4a963c4896a420d187d5e293 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 16 Apr 2023 22:59:05 -0400 Subject: [PATCH] VoIP: Add is active call binary sensor (#91486) * Refactor VoIP integration for more entities * Add active call binary sensor * Add actually missing binary sensor files * Improve test coverage --- homeassistant/components/voip/__init__.py | 9 +- .../components/voip/binary_sensor.py | 60 +++++++ homeassistant/components/voip/devices.py | 151 +++++++++++++----- homeassistant/components/voip/entity.py | 15 +- homeassistant/components/voip/strings.json | 5 + homeassistant/components/voip/switch.py | 19 +-- homeassistant/components/voip/voip.py | 17 +- .../components/webhook/translations/en.json | 2 +- tests/components/voip/conftest.py | 14 +- tests/components/voip/test_binary_sensor.py | 25 +++ tests/components/voip/test_devices.py | 30 +++- tests/components/voip/test_init.py | 3 - tests/components/voip/test_switch.py | 28 +++- tests/components/voip/test_voip.py | 17 +- 14 files changed, 308 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/voip/binary_sensor.py create mode 100644 tests/components/voip/test_binary_sensor.py diff --git a/homeassistant/components/voip/__init__.py b/homeassistant/components/voip/__init__.py index dddb2723102de..9328555505d03 100644 --- a/homeassistant/components/voip/__init__.py +++ b/homeassistant/components/voip/__init__.py @@ -17,7 +17,10 @@ from .devices import VoIPDevices from .voip import HassVoipDatagramProtocol -PLATFORMS = (Platform.SWITCH,) +PLATFORMS = ( + Platform.BINARY_SENSOR, + Platform.SWITCH, +) _LOGGER = logging.getLogger(__name__) _IP_WILDCARD = "0.0.0.0" @@ -25,6 +28,7 @@ "DOMAIN", "async_setup_entry", "async_unload_entry", + "async_remove_config_entry_device", ] @@ -39,6 +43,7 @@ class DomainData: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up VoIP integration from a config entry.""" devices = VoIPDevices(hass, entry) + devices.async_setup() transport = await _create_sip_server( hass, lambda: HassVoipDatagramProtocol(hass, devices), @@ -79,5 +84,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: - """Remove config entry from a device.""" + """Remove device from a config entry.""" return True diff --git a/homeassistant/components/voip/binary_sensor.py b/homeassistant/components/voip/binary_sensor.py new file mode 100644 index 0000000000000..70ecb87098428 --- /dev/null +++ b/homeassistant/components/voip/binary_sensor.py @@ -0,0 +1,60 @@ +"""Binary sensor for VoIP.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .devices import VoIPDevice +from .entity import VoIPEntity + +if TYPE_CHECKING: + from . import DomainData + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up VoIP binary sensor entities.""" + domain_data: DomainData = hass.data[DOMAIN] + + @callback + def async_add_device(device: VoIPDevice) -> None: + """Add device.""" + async_add_entities([VoIPCallActive(device)]) + + domain_data.devices.async_add_new_device_listener(async_add_device) + + async_add_entities([VoIPCallActive(device) for device in domain_data.devices]) + + +class VoIPCallActive(VoIPEntity, BinarySensorEntity): + """Entity to represent voip is allowed.""" + + entity_description = BinarySensorEntityDescription( + key="call_active", + translation_key="call_active", + ) + _attr_is_on = False + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + + self.async_on_remove(self._device.async_listen_update(self._is_active_changed)) + + @callback + def _is_active_changed(self, device: VoIPDevice) -> None: + """Call when active state changed.""" + self._attr_is_on = self._device.is_active + self.async_write_ha_state() diff --git a/homeassistant/components/voip/devices.py b/homeassistant/components/voip/devices.py index 3f853369b33cc..8b691e855e313 100644 --- a/homeassistant/components/voip/devices.py +++ b/homeassistant/components/voip/devices.py @@ -1,17 +1,61 @@ """Class to manage devices.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterator +from dataclasses import dataclass, field from voip_utils import CallInfo from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN +@dataclass +class VoIPDevice: + """Class to store device.""" + + voip_id: str + device_id: str + is_active: bool = False + update_listeners: list[Callable[[VoIPDevice], None]] = field(default_factory=list) + + @callback + def set_is_active(self, active: bool) -> None: + """Set active state.""" + self.is_active = active + for listener in self.update_listeners: + listener(self) + + @callback + def async_listen_update( + self, listener: Callable[[VoIPDevice], None] + ) -> Callable[[], None]: + """Listen for updates.""" + self.update_listeners.append(listener) + return lambda: self.update_listeners.remove(listener) + + @callback + def async_allow_call(self, hass: HomeAssistant) -> bool: + """Return if call is allowed.""" + ent_reg = er.async_get(hass) + + allowed_call_entity_id = ent_reg.async_get_entity_id( + "switch", DOMAIN, f"{self.voip_id}-allow_call" + ) + # If 2 requests come in fast, the device registry entry has been created + # but entity might not exist yet. + if allowed_call_entity_id is None: + return False + + if state := hass.states.get(allowed_call_entity_id): + return state.state == "on" + + return False + + class VoIPDevices: """Class to store devices.""" @@ -19,21 +63,53 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize VoIP devices.""" self.hass = hass self.config_entry = config_entry - self._new_device_listeners: list[Callable[[dr.DeviceEntry], None]] = [] + self._new_device_listeners: list[Callable[[VoIPDevice], None]] = [] + self.devices: dict[str, VoIPDevice] = {} + + @callback + def async_setup(self) -> None: + """Set up devices.""" + for device in dr.async_entries_for_config_entry( + dr.async_get(self.hass), self.config_entry.entry_id + ): + voip_id = next( + (item[1] for item in device.identifiers if item[0] == DOMAIN), None + ) + if voip_id is None: + continue + self.devices[voip_id] = VoIPDevice( + voip_id=voip_id, + device_id=device.id, + ) + + @callback + def async_device_removed(ev: Event) -> None: + """Handle device removed.""" + removed_id = ev.data["device_id"] + self.devices = { + voip_id: voip_device + for voip_id, voip_device in self.devices.items() + if voip_device.device_id != removed_id + } + + self.config_entry.async_on_unload( + self.hass.bus.async_listen( + dr.EVENT_DEVICE_REGISTRY_UPDATED, + async_device_removed, + callback(lambda ev: ev.data.get("action") == "remove"), + ) + ) @callback def async_add_new_device_listener( - self, listener: Callable[[dr.DeviceEntry], None] + self, listener: Callable[[VoIPDevice], None] ) -> None: """Add a new device listener.""" self._new_device_listeners.append(listener) @callback - def async_allow_call(self, call_info: CallInfo) -> bool: - """Check if a call is allowed.""" - dev_reg = dr.async_get(self.hass) - ip_address = call_info.caller_ip - + def async_get_or_create(self, call_info: CallInfo) -> VoIPDevice: + """Get or create a device.""" user_agent = call_info.headers.get("user-agent", "") user_agent_parts = user_agent.split() if len(user_agent_parts) == 3 and user_agent_parts[0] == "Grandstream": @@ -45,35 +121,34 @@ def async_allow_call(self, call_info: CallInfo) -> bool: model = user_agent if user_agent else None fw_version = None - device = dev_reg.async_get_device({(DOMAIN, ip_address)}) - - if device is None: - device = dev_reg.async_get_or_create( - config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, ip_address)}, - name=ip_address, - manufacturer=manuf, - model=model, - sw_version=fw_version, - ) - for listener in self._new_device_listeners: - listener(device) - return False - - if fw_version is not None and device.sw_version != fw_version: - dev_reg.async_update_device(device.id, sw_version=fw_version) - - ent_reg = er.async_get(self.hass) - - allowed_call_entity_id = ent_reg.async_get_entity_id( - "switch", DOMAIN, f"{ip_address}-allow_call" + dev_reg = dr.async_get(self.hass) + voip_id = call_info.caller_ip + voip_device = self.devices.get(voip_id) + + if voip_device is not None: + device = dev_reg.async_get(voip_device.device_id) + if device and fw_version and device.sw_version != fw_version: + dev_reg.async_update_device(device.id, sw_version=fw_version) + + return voip_device + + device = dev_reg.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={(DOMAIN, voip_id)}, + name=voip_id, + manufacturer=manuf, + model=model, + sw_version=fw_version, ) - # If 2 requests come in fast, the device registry entry has been created - # but entity might not exist yet. - if allowed_call_entity_id is None: - return False + voip_device = self.devices[voip_id] = VoIPDevice( + voip_id=voip_id, + device_id=device.id, + ) + for listener in self._new_device_listeners: + listener(voip_device) - if state := self.hass.states.get(allowed_call_entity_id): - return state.state == "on" + return voip_device - return False + def __iter__(self) -> Iterator[VoIPDevice]: + """Iterate over devices.""" + return iter(self.devices.values()) diff --git a/homeassistant/components/voip/entity.py b/homeassistant/components/voip/entity.py index 1114e93354577..9b3cc641a66b3 100644 --- a/homeassistant/components/voip/entity.py +++ b/homeassistant/components/voip/entity.py @@ -2,10 +2,10 @@ from __future__ import annotations -from homeassistant.const import EntityCategory -from homeassistant.helpers import device_registry as dr, entity +from homeassistant.helpers import entity from .const import DOMAIN +from .devices import VoIPDevice class VoIPEntity(entity.Entity): @@ -13,14 +13,11 @@ class VoIPEntity(entity.Entity): _attr_has_entity_name = True _attr_should_poll = False - _attr_entity_category = EntityCategory.CONFIG - def __init__(self, device: dr.DeviceEntry) -> None: + def __init__(self, device: VoIPDevice) -> None: """Initialize VoIP entity.""" - ip_address: str = next( - item[1] for item in device.identifiers if item[0] == DOMAIN - ) - self._attr_unique_id = f"{ip_address}-{self.entity_description.key}" + self._device = device + self._attr_unique_id = f"{device.voip_id}-{self.entity_description.key}" self._attr_device_info = entity.DeviceInfo( - identifiers={(DOMAIN, ip_address)}, + identifiers={(DOMAIN, device.voip_id)}, ) diff --git a/homeassistant/components/voip/strings.json b/homeassistant/components/voip/strings.json index cd221790cc498..dc3dd8a43cc75 100644 --- a/homeassistant/components/voip/strings.json +++ b/homeassistant/components/voip/strings.json @@ -15,6 +15,11 @@ } }, "entity": { + "binary_sensor": { + "call_active": { + "name": "Call Active" + } + }, "switch": { "allow_call": { "name": "Allow Calls" diff --git a/homeassistant/components/voip/switch.py b/homeassistant/components/voip/switch.py index 0a9a91c071706..f8484241fc51e 100644 --- a/homeassistant/components/voip/switch.py +++ b/homeassistant/components/voip/switch.py @@ -6,12 +6,13 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, restore_state +from homeassistant.helpers import restore_state from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN +from .devices import VoIPDevice from .entity import VoIPEntity if TYPE_CHECKING: @@ -27,20 +28,14 @@ async def async_setup_entry( domain_data: DomainData = hass.data[DOMAIN] @callback - def async_add_device(device: dr.DeviceEntry) -> None: + def async_add_device(device: VoIPDevice) -> None: """Add device.""" async_add_entities([VoIPCallAllowedSwitch(device)]) domain_data.devices.async_add_new_device_listener(async_add_device) async_add_entities( - [ - VoIPCallAllowedSwitch(device) - for device in dr.async_entries_for_config_entry( - dr.async_get(hass), - config_entry.entry_id, - ) - ] + [VoIPCallAllowedSwitch(device) for device in domain_data.devices] ) @@ -48,7 +43,9 @@ class VoIPCallAllowedSwitch(VoIPEntity, restore_state.RestoreEntity, SwitchEntit """Entity to represent voip is allowed.""" entity_description = SwitchEntityDescription( - key="allow_call", translation_key="allow_call" + key="allow_call", + translation_key="allow_call", + entity_category=EntityCategory.CONFIG, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 4224123d14e4e..073ff690b7b7c 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant if TYPE_CHECKING: - from .devices import VoIPDevices + from .devices import VoIPDevice, VoIPDevices _LOGGER = logging.getLogger(__name__) @@ -41,13 +41,16 @@ def __init__(self, hass: HomeAssistant, devices: VoIPDevices) -> None: protocol_factory=lambda call_info: PipelineRtpDatagramProtocol( hass, hass.config.language, + devices.async_get_or_create(call_info), ), ) + self.hass = hass self.devices = devices def is_valid_call(self, call_info: CallInfo) -> bool: """Filter calls.""" - return self.devices.async_allow_call(call_info) + device = self.devices.async_get_or_create(call_info) + return device.async_allow_call(self.hass) class PipelineRtpDatagramProtocol(RtpDatagramProtocol): @@ -57,6 +60,7 @@ def __init__( self, hass: HomeAssistant, language: str, + voip_device: VoIPDevice, pipeline_timeout: float = 30.0, audio_timeout: float = 2.0, ) -> None: @@ -66,6 +70,7 @@ def __init__( self.hass = hass self.language = language + self.voip_device = voip_device self.pipeline: Pipeline | None = None self.pipeline_timeout = pipeline_timeout self.audio_timeout = audio_timeout @@ -76,7 +81,13 @@ def __init__( def connection_made(self, transport): """Server is ready.""" - self.transport = transport + super().connection_made(transport) + self.voip_device.set_is_active(True) + + def connection_lost(self, exc): + """Handle connection is lost or closed.""" + super().connection_lost(exc) + self.voip_device.set_is_active(False) def on_chunk(self, audio_bytes: bytes) -> None: """Handle raw audio chunk.""" diff --git a/homeassistant/components/webhook/translations/en.json b/homeassistant/components/webhook/translations/en.json index 619ef88203f95..2ef7bc86c0bff 100644 --- a/homeassistant/components/webhook/translations/en.json +++ b/homeassistant/components/webhook/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "trigger_missing_local_only": { - "description": "A choice needs to be made about whether the {webhook_id} webhook automation trigger is accessible from the internet. Edit the {automation_name} automation, and click the gear icon beside the Webhook ID to choose a value for 'Only accessible from the local network'", + "description": "A choice needs to be made about whether the {webhook_id} webhook automation trigger is accessible from the internet. [Edit the automation]({edit}) \"{automation_name}\", (`{entity_id}`) and click the gear icon beside the Webhook ID to choose a value for 'Only accessible from the local network'", "title": "Update webhook trigger: {webhook_id}" } } diff --git a/tests/components/voip/conftest.py b/tests/components/voip/conftest.py index 20aa5ac6a947e..80d8eaa11c31a 100644 --- a/tests/components/voip/conftest.py +++ b/tests/components/voip/conftest.py @@ -7,7 +7,8 @@ import pytest from voip_utils import CallInfo -from homeassistant.components.voip import DOMAIN, VoIPDevices +from homeassistant.components.voip import DOMAIN +from homeassistant.components.voip.devices import VoIPDevice, VoIPDevices from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -62,3 +63,14 @@ def call_info() -> CallInfo: "content-length": "480", }, ) + + +@pytest.fixture +async def voip_device( + hass: HomeAssistant, voip_devices: VoIPDevices, call_info: CallInfo +) -> VoIPDevice: + """Get a VoIP device fixture.""" + device = voip_devices.async_get_or_create(call_info) + # to make sure all platforms are set up + await hass.async_block_till_done() + return device diff --git a/tests/components/voip/test_binary_sensor.py b/tests/components/voip/test_binary_sensor.py new file mode 100644 index 0000000000000..67a0bef0f6255 --- /dev/null +++ b/tests/components/voip/test_binary_sensor.py @@ -0,0 +1,25 @@ +"""Test VoIP binary sensor devices.""" +from homeassistant.components.voip.devices import VoIPDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_allow_call( + hass: HomeAssistant, + config_entry: ConfigEntry, + voip_device: VoIPDevice, +) -> None: + """Test allow call.""" + state = hass.states.get("binary_sensor.192_168_1_210_call_active") + assert state is not None + assert state.state == "off" + + voip_device.set_is_active(True) + + state = hass.states.get("binary_sensor.192_168_1_210_call_active") + assert state.state == "on" + + voip_device.set_is_active(False) + + state = hass.states.get("binary_sensor.192_168_1_210_call_active") + assert state.state == "off" diff --git a/tests/components/voip/test_devices.py b/tests/components/voip/test_devices.py index 9918144e527fa..af5b176281e4a 100644 --- a/tests/components/voip/test_devices.py +++ b/tests/components/voip/test_devices.py @@ -4,7 +4,8 @@ from voip_utils import CallInfo -from homeassistant.components.voip import DOMAIN, VoIPDevices +from homeassistant.components.voip import DOMAIN +from homeassistant.components.voip.devices import VoIPDevice, VoIPDevices from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry @@ -16,7 +17,8 @@ async def test_device_registry_info( device_registry: DeviceRegistry, ) -> None: """Test info in device registry.""" - assert not voip_devices.async_allow_call(call_info) + voip_device = voip_devices.async_get_or_create(call_info) + assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) assert device is not None @@ -27,8 +29,9 @@ async def test_device_registry_info( # Test we update the device if the fw updates call_info.headers["user-agent"] = "Grandstream HT801 2.0.0.0" + voip_device = voip_devices.async_get_or_create(call_info) - assert not voip_devices.async_allow_call(call_info) + assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) assert device.sw_version == "2.0.0.0" @@ -42,9 +45,28 @@ async def test_device_registry_info_from_unknown_phone( ) -> None: """Test info in device registry from unknown phone.""" call_info.headers["user-agent"] = "Unknown" - assert not voip_devices.async_allow_call(call_info) + voip_device = voip_devices.async_get_or_create(call_info) + assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) assert device.manufacturer is None assert device.model == "Unknown" assert device.sw_version is None + + +async def test_remove_device_registry_entry( + hass: HomeAssistant, + voip_device: VoIPDevice, + voip_devices: VoIPDevices, + device_registry: DeviceRegistry, +) -> None: + """Test removing a device registry entry.""" + assert voip_device.voip_id in voip_devices.devices + assert hass.states.get("switch.192_168_1_210_allow_calls") is not None + + device_registry.async_remove_device(voip_device.device_id) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get("switch.192_168_1_210_allow_calls") is None + assert voip_device.voip_id not in voip_devices.devices diff --git a/tests/components/voip/test_init.py b/tests/components/voip/test_init.py index 77c14260f8e26..e1f02c50a5d6b 100644 --- a/tests/components/voip/test_init.py +++ b/tests/components/voip/test_init.py @@ -1,9 +1,6 @@ """Test VoIP init.""" from homeassistant.core import HomeAssistant -# socket_enabled, -# unused_udp_port_factory, - async def test_unload_entry( hass: HomeAssistant, diff --git a/tests/components/voip/test_switch.py b/tests/components/voip/test_switch.py index 5363fb8c2d68a..eb8fcfa2220bf 100644 --- a/tests/components/voip/test_switch.py +++ b/tests/components/voip/test_switch.py @@ -1,16 +1,16 @@ """Test VoIP switch devices.""" - -from __future__ import annotations - +from homeassistant.components.voip.devices import VoIPDevice +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant async def test_allow_call( - hass: HomeAssistant, config_entry, voip_devices, call_info + hass: HomeAssistant, + config_entry: ConfigEntry, + voip_device: VoIPDevice, ) -> None: """Test allow call.""" - assert not voip_devices.async_allow_call(call_info) - await hass.async_block_till_done() + assert not voip_device.async_allow_call(hass) state = hass.states.get("switch.192_168_1_210_allow_calls") assert state is not None @@ -28,13 +28,25 @@ async def test_allow_call( blocking=True, ) - assert voip_devices.async_allow_call(call_info) + assert voip_device.async_allow_call(hass) state = hass.states.get("switch.192_168_1_210_allow_calls") assert state.state == "on" await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("switch.192_168_1_210_allow_calls") - assert state is not None assert state.state == "on" + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.192_168_1_210_allow_calls"}, + blocking=True, + ) + + assert not voip_device.async_allow_call(hass) + + state = hass.states.get("switch.192_168_1_210_allow_calls") + assert state.state == "off" diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 9718fa6db3ee9..74eccdc438261 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -5,6 +5,7 @@ import async_timeout from homeassistant.components import assist_pipeline, voip +from homeassistant.components.voip.devices import VoIPDevice from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -12,7 +13,10 @@ _MEDIA_ID = "12345" -async def test_pipeline(hass: HomeAssistant) -> None: +async def test_pipeline( + hass: HomeAssistant, + voip_device: VoIPDevice, +) -> None: """Test that pipeline function is called from RTP protocol.""" assert await async_setup_component(hass, "voip", {}) @@ -80,8 +84,7 @@ async def async_get_media_source_audio( new=async_get_media_source_audio, ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( - hass, - hass.config.language, + hass, hass.config.language, voip_device ) rtp_protocol.transport = Mock() @@ -108,7 +111,7 @@ async def send_audio(*args, **kwargs): await done.wait() -async def test_pipeline_timeout(hass: HomeAssistant) -> None: +async def test_pipeline_timeout(hass: HomeAssistant, voip_device: VoIPDevice) -> None: """Test timeout during pipeline run.""" assert await async_setup_component(hass, "voip", {}) @@ -122,7 +125,7 @@ async def async_pipeline_from_audio_stream(*args, **kwargs): new=async_pipeline_from_audio_stream, ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( - hass, hass.config.language, pipeline_timeout=0.001 + hass, hass.config.language, voip_device, pipeline_timeout=0.001 ) transport = Mock(spec=["close"]) rtp_protocol.connection_made(transport) @@ -138,7 +141,7 @@ async def async_pipeline_from_audio_stream(*args, **kwargs): await done.wait() -async def test_stt_stream_timeout(hass: HomeAssistant) -> None: +async def test_stt_stream_timeout(hass: HomeAssistant, voip_device: VoIPDevice) -> None: """Test timeout in STT stream during pipeline run.""" assert await async_setup_component(hass, "voip", {}) @@ -155,7 +158,7 @@ async def async_pipeline_from_audio_stream(*args, **kwargs): new=async_pipeline_from_audio_stream, ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( - hass, hass.config.language, audio_timeout=0.001 + hass, hass.config.language, voip_device, audio_timeout=0.001 ) transport = Mock(spec=["close"]) rtp_protocol.connection_made(transport)