mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Adjust unifiprotect to not access DeviceEntry.config_entries (#181766)
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user