Adjust unifiprotect to not access DeviceEntry.config_entries (#181766)

This commit is contained in:
Erik Montnemery
2026-09-11 12:17:38 +02:00
committed by GitHub
parent f07aac39de
commit bcb9bc5deb
5 changed files with 55 additions and 106 deletions
@@ -833,23 +833,6 @@ class ProtectData:
update_callback(obj)
@callback
def async_ufp_instance_for_config_entry_ids(
hass: HomeAssistant, config_entry_ids: set[str]
) -> ProtectApiClient | None:
"""Find the UFP instance for the config entry ids."""
return next(
iter(
entry.runtime_data.api
for entry_id in config_entry_ids
if (entry := hass.config_entries.async_get_entry(entry_id))
and entry.domain == DOMAIN
and hasattr(entry, "runtime_data")
),
None,
)
@callback
def async_get_ufp_entries(hass: HomeAssistant) -> list[UFPConfigEntry]:
"""Get all the UFP entries."""
@@ -25,6 +25,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
service,
)
from homeassistant.helpers.target import (
TargetSelection,
@@ -43,7 +44,7 @@ from .const import (
KEYRINGS_USER_FULL_NAME,
KEYRINGS_USER_STATUS,
)
from .data import async_ufp_instance_for_config_entry_ids
from .data import UFPConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -104,35 +105,24 @@ PTZ_GOTO_PRESET_SCHEMA = vol.Schema(
@callback
def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient:
device_registry = dr.async_get(hass)
if not (device_entry := device_registry.async_get(device_id)):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
device_entry = device_registry.async_get(device_id)
if isinstance(device_entry, dr.ChildDeviceEntry):
return _async_get_ufp_instance(hass, device_entry.parent_device_id)
if device_entry.via_device_id is not None:
if device_entry is not None and device_entry.via_device_id is not None:
return _async_get_ufp_instance(hass, device_entry.via_device_id)
config_entry_ids = device_entry.config_entries
if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
if ufp_instance.is_public_only:
# Actions read/write through the private bootstrap, which an
# API-key-only entry never initializes.
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="public_only_no_actions",
)
return ufp_instance
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
_, config_entry = service.async_get_device_and_config_entry(hass, DOMAIN, device_id)
ufp_instance = cast(UFPConfigEntry, config_entry).runtime_data.api
if ufp_instance.is_public_only:
# Actions read/write through the private bootstrap, which an
# API-key-only entry never initializes.
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="public_only_no_actions",
)
return ufp_instance
@callback
@@ -841,9 +841,6 @@
"command_error": {
"message": "Error communicating with UniFi Protect while sending command: {error}"
},
"device_not_found": {
"message": "No device found for device id: {device_id}"
},
"entry_auth_failed": {
"message": "Authentication failed, please reauthenticate"
},
+1 -59
View File
@@ -25,10 +25,7 @@ from homeassistant.components.unifiprotect.const import (
PLATFORMS,
PUBLIC_ONLY_PLATFORMS,
)
from homeassistant.components.unifiprotect.data import (
ProtectData,
async_ufp_instance_for_config_entry_ids,
)
from homeassistant.components.unifiprotect.data import ProtectData
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_KEY, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
@@ -474,61 +471,6 @@ async def test_remove_config_entry_device_rejects_child_device(
assert device_registry.async_get(child_device.id)
@pytest.mark.parametrize(
("mock_entries", "expected_result"),
[
pytest.param(
[
MockConfigEntry(
domain=DOMAIN,
entry_id="1",
data={},
),
MockConfigEntry(
domain="other_domain",
entry_id="2",
data={},
),
],
"mock_api_instance_1",
id="one_matching_domain",
),
pytest.param(
[
MockConfigEntry(
domain="other_domain",
entry_id="1",
data={},
),
MockConfigEntry(
domain="other_domain",
entry_id="2",
data={},
),
],
None,
id="no_matching_domain",
),
],
)
async def test_async_ufp_instance_for_config_entry_ids(
hass: HomeAssistant,
mock_entries: list[MockConfigEntry],
expected_result: str | None,
) -> None:
"""Test async_ufp_instance_for_config_entry_ids with various configs."""
for index, entry in enumerate(mock_entries):
entry.add_to_hass(hass)
entry.runtime_data = Mock(api=f"mock_api_instance_{index + 1}")
entry_ids = {entry.entry_id for entry in mock_entries}
result = async_ufp_instance_for_config_entry_ids(hass, entry_ids)
assert result == expected_result
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
async def test_setup_creates_api_key_when_missing(
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
+40 -3
View File
@@ -30,7 +30,7 @@ from homeassistant.components.unifiprotect.services import (
)
from homeassistant.config_entries import ConfigEntryDisabler
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_NAME
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -38,6 +38,8 @@ from . import patch_ufp_method
from .conftest import UNIFI_MAC
from .utils import MockUFPFixture, init_entry
from tests.common import MockConfigEntry
@pytest.fixture(name="device")
async def device_fixture(
@@ -69,12 +71,13 @@ async def test_global_service_bad_device(
) -> None:
"""Test global service, invalid device ID."""
await init_entry(hass, ufp, [])
nvr = ufp.api.bootstrap.nvr
with patch_ufp_method(
nvr, "add_custom_doorbell_message", new_callable=AsyncMock
) as mock_method:
with pytest.raises(HomeAssistantError):
with pytest.raises(ServiceValidationError) as error:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_DOORBELL_TEXT,
@@ -83,6 +86,37 @@ async def test_global_service_bad_device(
)
assert not mock_method.called
assert error.value.translation_domain == HOMEASSISTANT_DOMAIN
assert error.value.translation_key == "service_device_not_found"
async def test_global_service_device_from_other_integration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
ufp: MockUFPFixture,
) -> None:
"""Test a device not owned by a UniFi Protect config entry is rejected."""
await init_entry(hass, ufp, [])
other_entry = MockConfigEntry(domain="other")
other_entry.add_to_hass(hass)
other_device = device_registry.async_get_or_create(
config_entry_id=other_entry.entry_id,
identifiers={("other", "other-device")},
name="Other device",
)
with pytest.raises(ServiceValidationError) as error:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_DOORBELL_TEXT,
{ATTR_DEVICE_ID: other_device.id, ATTR_MESSAGE: "Test Message"},
blocking=True,
)
assert error.value.translation_domain == HOMEASSISTANT_DOMAIN
assert error.value.translation_key == "service_device_wrong_domain"
async def test_global_service_exception(
hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture
@@ -159,7 +193,7 @@ async def test_add_doorbell_text_disabled_config_entry(
with patch_ufp_method(
nvr, "add_custom_doorbell_message", new_callable=AsyncMock
) as mock_method:
with pytest.raises(HomeAssistantError):
with pytest.raises(ServiceValidationError) as error:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_DOORBELL_TEXT,
@@ -168,6 +202,9 @@ async def test_add_doorbell_text_disabled_config_entry(
)
assert not mock_method.called
assert error.value.translation_domain == HOMEASSISTANT_DOMAIN
assert error.value.translation_key == "service_config_entry_not_loaded"
async def test_set_chime_paired_doorbells(
hass: HomeAssistant,