From 79ee2e954bd716f78a8a3190f78ade304d3170f0 Mon Sep 17 00:00:00 2001 From: Jamin Date: Wed, 15 Jan 2025 19:59:58 -0600 Subject: [PATCH] Use SIP URI for VoIP device identifier (#135603) * Use SIP URI for VoIP device identifier Use the SIP URI instead of just host/IP address to identify VoIP devices. This will allow calls initiating from Home Assistant to the device as well as allows devices connecting through a PBX to be uniquely identified. * Add tests --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/voip/devices.py | 25 +++++++--- tests/components/voip/test_binary_sensor.py | 14 +++--- tests/components/voip/test_devices.py | 51 ++++++++++++++++++--- tests/components/voip/test_select.py | 4 +- tests/components/voip/test_switch.py | 14 +++--- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/voip/devices.py b/homeassistant/components/voip/devices.py index 613d05fc614..163cb445340 100644 --- a/homeassistant/components/voip/devices.py +++ b/homeassistant/components/voip/devices.py @@ -136,16 +136,23 @@ class VoIPDevices: fw_version = None dev_reg = dr.async_get(self.hass) - voip_id = call_info.caller_ip + if call_info.caller_endpoint is None: + raise RuntimeError("Could not identify VOIP caller") + voip_id = call_info.caller_endpoint.uri 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 + if voip_device is None: + # If we couldn't find the device based on SIP URI, see if we can + # find an old device based on just the host/IP and migrate it + voip_device = self.devices.get(call_info.caller_endpoint.host) + if voip_device is not None: + voip_device.voip_id = voip_id + self.devices[voip_id] = voip_device + dev_reg.async_update_device( + voip_device.device_id, new_identifiers={(DOMAIN, voip_id)} + ) + # Update device with latest info device = dev_reg.async_get_or_create( config_entry_id=self.config_entry.entry_id, identifiers={(DOMAIN, voip_id)}, @@ -155,6 +162,10 @@ class VoIPDevices: sw_version=fw_version, configuration_url=f"http://{call_info.caller_ip}", ) + + if voip_device is not None: + return voip_device + voip_device = self.devices[voip_id] = VoIPDevice( voip_id=voip_id, device_id=device.id, diff --git a/tests/components/voip/test_binary_sensor.py b/tests/components/voip/test_binary_sensor.py index 44ac8e4d77f..55d8ac4473c 100644 --- a/tests/components/voip/test_binary_sensor.py +++ b/tests/components/voip/test_binary_sensor.py @@ -22,18 +22,18 @@ async def test_call_in_progress( voip_device: VoIPDevice, ) -> None: """Test call in progress.""" - state = hass.states.get("binary_sensor.192_168_1_210_call_in_progress") + state = hass.states.get("binary_sensor.sip_192_168_1_210_5060_call_in_progress") 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_in_progress") + state = hass.states.get("binary_sensor.sip_192_168_1_210_5060_call_in_progress") assert state.state == "on" voip_device.set_is_active(False) - state = hass.states.get("binary_sensor.192_168_1_210_call_in_progress") + state = hass.states.get("binary_sensor.sip_192_168_1_210_5060_call_in_progress") assert state.state == "off" @@ -45,9 +45,9 @@ async def test_assist_in_progress_disabled_by_default( ) -> None: """Test assist in progress binary sensor is added disabled.""" - assert not hass.states.get("binary_sensor.192_168_1_210_call_in_progress") + assert not hass.states.get("binary_sensor.sip_192_168_1_210_5060_call_in_progress") entity_entry = entity_registry.async_get( - "binary_sensor.192_168_1_210_call_in_progress" + "binary_sensor.sip_192_168_1_210_5060_call_in_progress" ) assert entity_entry assert entity_entry.disabled @@ -63,7 +63,7 @@ async def test_assist_in_progress_issue( ) -> None: """Test assist in progress binary sensor.""" - call_in_progress_entity_id = "binary_sensor.192_168_1_210_call_in_progress" + call_in_progress_entity_id = "binary_sensor.sip_192_168_1_210_5060_call_in_progress" state = hass.states.get(call_in_progress_entity_id) assert state is not None @@ -96,7 +96,7 @@ async def test_assist_in_progress_repair_flow( ) -> None: """Test assist in progress binary sensor deprecation issue flow.""" - call_in_progress_entity_id = "binary_sensor.192_168_1_210_call_in_progress" + call_in_progress_entity_id = "binary_sensor.sip_192_168_1_210_5060_call_in_progress" state = hass.states.get(call_in_progress_entity_id) assert state is not None diff --git a/tests/components/voip/test_devices.py b/tests/components/voip/test_devices.py index 55359b8407d..d16ac76d290 100644 --- a/tests/components/voip/test_devices.py +++ b/tests/components/voip/test_devices.py @@ -2,6 +2,7 @@ from __future__ import annotations +import pytest from voip_utils import CallInfo from homeassistant.components.voip import DOMAIN @@ -9,6 +10,8 @@ from homeassistant.components.voip.devices import VoIPDevice, VoIPDevices from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from tests.common import MockConfigEntry + async def test_device_registry_info( hass: HomeAssistant, @@ -21,10 +24,10 @@ async def test_device_registry_info( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device is not None - assert device.name == call_info.caller_ip + assert device.name == call_info.caller_endpoint.uri assert device.manufacturer == "Grandstream" assert device.model == "HT801" assert device.sw_version == "1.0.17.5" @@ -36,7 +39,7 @@ async def test_device_registry_info( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device.sw_version == "2.0.0.0" @@ -53,7 +56,7 @@ async def test_device_registry_info_from_unknown_phone( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device.manufacturer is None assert device.model == "Unknown" @@ -68,11 +71,47 @@ async def test_remove_device_registry_entry( ) -> 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 + assert hass.states.get("switch.sip_192_168_1_210_5060_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 hass.states.get("switch.sip_192_168_1_210_5060_allow_calls") is None assert voip_device.voip_id not in voip_devices.devices + + +@pytest.fixture +async def legacy_dev_reg_entry( + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, + call_info: CallInfo, +) -> None: + """Fixture to run before we set up the VoIP integration via fixture.""" + return device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, call_info.caller_ip)}, + ) + + +async def test_device_registry_migation( + hass: HomeAssistant, + legacy_dev_reg_entry: dr.DeviceEntry, + voip_devices: VoIPDevices, + call_info: CallInfo, + device_registry: dr.DeviceRegistry, +) -> None: + """Test info in device registry migrates old devices.""" + voip_device = voip_devices.async_get_or_create(call_info) + assert voip_device.voip_id == call_info.caller_endpoint.uri + + device = device_registry.async_get_device( + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} + ) + assert device is not None + assert device.id == legacy_dev_reg_entry.id + assert device.identifiers == {(DOMAIN, call_info.caller_endpoint.uri)} + assert device.name == call_info.caller_endpoint.uri + assert device.manufacturer == "Grandstream" + assert device.model == "HT801" + assert device.sw_version == "1.0.17.5" diff --git a/tests/components/voip/test_select.py b/tests/components/voip/test_select.py index 78bb8d6c6b4..1b45c739535 100644 --- a/tests/components/voip/test_select.py +++ b/tests/components/voip/test_select.py @@ -15,7 +15,7 @@ async def test_pipeline_select( Functionality is tested in assist_pipeline/test_select.py. This test is only to ensure it is set up. """ - state = hass.states.get("select.192_168_1_210_assistant") + state = hass.states.get("select.sip_192_168_1_210_5060_assistant") assert state is not None assert state.state == "preferred" @@ -30,6 +30,6 @@ async def test_vad_sensitivity_select( Functionality is tested in assist_pipeline/test_select.py. This test is only to ensure it is set up. """ - state = hass.states.get("select.192_168_1_210_finished_speaking_detection") + state = hass.states.get("select.sip_192_168_1_210_5060_finished_speaking_detection") assert state is not None assert state.state == "default" diff --git a/tests/components/voip/test_switch.py b/tests/components/voip/test_switch.py index 8b3cd03f2ac..ac331ed01a7 100644 --- a/tests/components/voip/test_switch.py +++ b/tests/components/voip/test_switch.py @@ -13,41 +13,41 @@ async def test_allow_call( """Test allow call.""" assert not voip_device.async_allow_call(hass) - state = hass.states.get("switch.192_168_1_210_allow_calls") + state = hass.states.get("switch.sip_192_168_1_210_5060_allow_calls") assert state is not None assert state.state == "off" await hass.config_entries.async_reload(config_entry.entry_id) - state = hass.states.get("switch.192_168_1_210_allow_calls") + state = hass.states.get("switch.sip_192_168_1_210_5060_allow_calls") assert state.state == "off" await hass.services.async_call( "switch", "turn_on", - {"entity_id": "switch.192_168_1_210_allow_calls"}, + {"entity_id": "switch.sip_192_168_1_210_5060_allow_calls"}, blocking=True, ) assert voip_device.async_allow_call(hass) - state = hass.states.get("switch.192_168_1_210_allow_calls") + state = hass.states.get("switch.sip_192_168_1_210_5060_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") + state = hass.states.get("switch.sip_192_168_1_210_5060_allow_calls") assert state.state == "on" await hass.services.async_call( "switch", "turn_off", - {"entity_id": "switch.192_168_1_210_allow_calls"}, + {"entity_id": "switch.sip_192_168_1_210_5060_allow_calls"}, blocking=True, ) assert not voip_device.async_allow_call(hass) - state = hass.states.get("switch.192_168_1_210_allow_calls") + state = hass.states.get("switch.sip_192_168_1_210_5060_allow_calls") assert state.state == "off"