diff --git a/homeassistant/components/voip/__init__.py b/homeassistant/components/voip/__init__.py index 07cdccdca56..9ea202e3b57 100644 --- a/homeassistant/components/voip/__init__.py +++ b/homeassistant/components/voip/__init__.py @@ -8,6 +8,7 @@ import logging from voip_utils import SIP_PORT +from homeassistant.auth.const import GROUP_ID_USER from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -43,6 +44,18 @@ class DomainData: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up VoIP integration from a config entry.""" + # Make sure there is a valid user ID for VoIP in the config entry + if ( + "user" not in entry.data + or (await hass.auth.async_get_user(entry.data["user"])) is None + ): + voip_user = await hass.auth.async_create_system_user( + "Voice over IP", group_ids=[GROUP_ID_USER] + ) + hass.config_entries.async_update_entry( + entry, data={**entry.data, "user": voip_user.id} + ) + devices = VoIPDevices(hass, entry) devices.async_setup() transport = await _create_sip_server( @@ -87,3 +100,11 @@ async def async_remove_config_entry_device( ) -> bool: """Remove device from a config entry.""" return True + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove VoIP entry.""" + if "user" in entry.data and ( + user := await hass.auth.async_get_user(entry.data["user"]) + ): + await hass.auth.async_remove_user(user) diff --git a/homeassistant/components/voip/strings.json b/homeassistant/components/voip/strings.json index b483be494f2..6eb9d36df73 100644 --- a/homeassistant/components/voip/strings.json +++ b/homeassistant/components/voip/strings.json @@ -2,16 +2,11 @@ "config": { "step": { "user": { - "data": { - "ip_address": "IP Address" - } + "description": "Receive Voice over IP calls to interact with Assist." } }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, - "error": { - "invalid_ip_address": "Invalid IPv4 address." } }, "entity": { diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 91a4a1c8bd4..6a18ec6696b 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -55,6 +55,7 @@ class HassVoipDatagramProtocol(VoipDatagramProtocol): hass, hass.config.language, devices.async_get_or_create(call_info), + Context(user_id=devices.config_entry.data["user"]), ), invalid_protocol_factory=lambda call_info: NotConfiguredRtpDatagramProtocol( hass, @@ -77,6 +78,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): hass: HomeAssistant, language: str, voip_device: VoIPDevice, + context: Context, pipeline_timeout: float = 30.0, audio_timeout: float = 2.0, listening_tone_enabled: bool = True, @@ -94,7 +96,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): self.listening_tone_enabled = listening_tone_enabled self._audio_queue: asyncio.Queue[bytes] = asyncio.Queue() - self._context = Context() + self._context = context self._conversation_id: str | None = None self._pipeline_task: asyncio.Task | None = None self._session_id: str | None = None diff --git a/tests/components/voip/snapshots/test_init.ambr b/tests/components/voip/snapshots/test_init.ambr new file mode 100644 index 00000000000..319276ffcd7 --- /dev/null +++ b/tests/components/voip/snapshots/test_init.ambr @@ -0,0 +1,13 @@ +# serializer version: 1 +# name: test_user_management + list([ + dict({ + 'id': 'system-users', + 'name': 'Users', + 'policy': dict({ + 'entities': True, + }), + 'system_generated': True, + }), + ]) +# --- diff --git a/tests/components/voip/test_init.py b/tests/components/voip/test_init.py index e1f02c50a5d..edc55685597 100644 --- a/tests/components/voip/test_init.py +++ b/tests/components/voip/test_init.py @@ -1,4 +1,6 @@ """Test VoIP init.""" +from syrupy.assertion import SnapshotAssertion + from homeassistant.core import HomeAssistant @@ -9,3 +11,22 @@ async def test_unload_entry( ) -> None: """Test adding/removing VoIP.""" assert await hass.config_entries.async_unload(config_entry.entry_id) + + +async def test_user_management( + hass: HomeAssistant, config_entry, setup_voip, snapshot: SnapshotAssertion +) -> None: + """Test creating and removing voip user.""" + user = await hass.auth.async_get_user(config_entry.data["user"]) + assert user is not None + assert user.is_active + assert user.system_generated + assert not user.is_admin + assert user.name == "Voice over IP" + assert user.groups == snapshot + assert len(user.credentials) == 0 + assert len(user.refresh_tokens) == 0 + + await hass.config_entries.async_remove(config_entry.entry_id) + + assert await hass.auth.async_get_user(user.id) is None diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 3a6b7bdc0c0..fe39fe9f567 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -6,7 +6,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.core import Context, HomeAssistant from homeassistant.setup import async_setup_component _ONE_SECOND = 16000 * 2 # 16Khz 16-bit @@ -86,6 +86,7 @@ async def test_pipeline( hass, hass.config.language, voip_device, + Context(), listening_tone_enabled=False, ) rtp_protocol.transport = Mock() @@ -133,6 +134,7 @@ async def test_pipeline_timeout(hass: HomeAssistant, voip_device: VoIPDevice) -> hass, hass.config.language, voip_device, + Context(), pipeline_timeout=0.001, listening_tone_enabled=False, ) @@ -170,6 +172,7 @@ async def test_stt_stream_timeout(hass: HomeAssistant, voip_device: VoIPDevice) hass, hass.config.language, voip_device, + Context(), audio_timeout=0.001, listening_tone_enabled=False, )