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 <balloob@gmail.com>
This commit is contained in:
Jamin 2025-01-15 19:59:58 -06:00 committed by GitHub
parent e736ca72f0
commit 79ee2e954b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 79 additions and 29 deletions

View File

@ -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,

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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"