Adjust monzo to not access DeviceEntry.config_entries (#181751)

This commit is contained in:
Erik Montnemery
2026-09-09 20:09:39 +02:00
committed by GitHub
parent 905e6a2f0f
commit 2166c17956
2 changed files with 116 additions and 73 deletions
+53 -68
View File
@@ -88,18 +88,7 @@ TRANSFER_SCHEMA = vol.Schema(
@callback
def _async_get_device(call: ServiceCall, field: str) -> dr.AnyDeviceEntry:
"""Get a selected device."""
if (device := dr.async_get(call.hass).async_get(call.data[field])) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device",
)
return device
@callback
def _async_get_resource_id(device: dr.AnyDeviceEntry) -> str:
def _async_get_resource_id(device: dr.DeviceEntry) -> str:
"""Get the Monzo resource ID represented by a device."""
for domain, resource_id in device.identifiers:
if domain == DOMAIN:
@@ -110,7 +99,7 @@ def _async_get_resource_id(device: dr.AnyDeviceEntry) -> str:
)
def _device_name(device: dr.AnyDeviceEntry) -> str:
def _device_name(device: dr.DeviceEntry) -> str:
"""Return the best available name for a device."""
return device.name_by_user or device.name or device.id
@@ -120,65 +109,61 @@ def _async_resolve_transfer(
call: ServiceCall,
) -> tuple[MonzoCoordinator, str, str]:
"""Resolve and validate the account and pot selected for a transfer."""
account_device = _async_get_device(call, ATTR_ACCOUNT)
pot_device = _async_get_device(call, ATTR_POT)
account_device, account_entry = service.async_get_device_and_config_entry(
call.hass, DOMAIN, call.data[ATTR_ACCOUNT]
)
pot_device, pot_entry = service.async_get_device_and_config_entry(
call.hass, DOMAIN, call.data[ATTR_POT]
)
if account_entry.entry_id != pot_entry.entry_id:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="different_entries",
)
account_id = _async_get_resource_id(account_device)
pot_id = _async_get_resource_id(pot_device)
for entry_id in account_device.config_entries & pot_device.config_entries:
config_entry = call.hass.config_entries.async_get_entry(entry_id)
if config_entry is None or config_entry.domain != DOMAIN:
continue
entry = cast(
MonzoConfigEntry,
service.async_get_config_entry(call.hass, DOMAIN, entry_id),
entry = cast(MonzoConfigEntry, account_entry)
coordinator = entry.runtime_data.coordinator
if (account := coordinator.data.accounts.get(account_id)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=(
"pot_selected_as_account"
if account_id in coordinator.data.pots
else "invalid_account"
),
translation_placeholders={"device_name": _device_name(account_device)},
)
coordinator = entry.runtime_data.coordinator
if (account := coordinator.data.accounts.get(account_id)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=(
"pot_selected_as_account"
if account_id in coordinator.data.pots
else "invalid_account"
),
translation_placeholders={"device_name": _device_name(account_device)},
)
if account["type"] in NON_TRANSFER_ACCOUNT_TYPES:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_transfer_account",
translation_placeholders={"device_name": _device_name(account_device)},
)
if (pot := coordinator.data.pots.get(pot_id)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=(
"account_selected_as_pot"
if pot_id in coordinator.data.accounts
else "invalid_pot"
),
translation_placeholders={"device_name": _device_name(pot_device)},
)
if pot["current_account_id"] != account_id:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="pot_account_mismatch",
translation_placeholders={
"account_name": account["name"],
"pot_name": pot["name"],
"pot_account_name": coordinator.data.accounts[
pot["current_account_id"]
]["name"],
},
)
return coordinator, account_id, pot_id
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="different_entries",
)
if account["type"] in NON_TRANSFER_ACCOUNT_TYPES:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_transfer_account",
translation_placeholders={"device_name": _device_name(account_device)},
)
if (pot := coordinator.data.pots.get(pot_id)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=(
"account_selected_as_pot"
if pot_id in coordinator.data.accounts
else "invalid_pot"
),
translation_placeholders={"device_name": _device_name(pot_device)},
)
if pot["current_account_id"] != account_id:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="pot_account_mismatch",
translation_placeholders={
"account_name": account["name"],
"pot_name": pot["name"],
"pot_account_name": coordinator.data.accounts[
pot["current_account_id"]
]["name"],
},
)
return coordinator, account_id, pot_id
async def _async_transfer(
+63 -5
View File
@@ -23,7 +23,8 @@ from homeassistant.components.monzo.services import (
SERVICE_DEPOSIT_INTO_POT,
SERVICE_WITHDRAW_FROM_POT,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Context, HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
@@ -218,7 +219,7 @@ async def test_missing_device(
transfer_devices: TransferDevices,
) -> None:
"""Test a missing selected device is rejected."""
with pytest.raises(ServiceValidationError):
with pytest.raises(ServiceValidationError) as error:
await hass.services.async_call(
DOMAIN,
SERVICE_DEPOSIT_INTO_POT,
@@ -230,22 +231,76 @@ async def test_missing_device(
blocking=True,
)
assert error.value.translation_domain == HOMEASSISTANT_DOMAIN
assert error.value.translation_key == "service_device_not_found"
async def test_devices_from_different_entries(
async def test_device_from_other_integration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
transfer_devices: TransferDevices,
) -> None:
"""Test the account and pot must belong to the same config entry."""
"""Test a device not owned by a Monzo config entry is rejected."""
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_DEPOSIT_INTO_POT,
{
ATTR_ACCOUNT: transfer_devices.account_device_id,
ATTR_POT: other_device.id,
ATTR_AMOUNT: 1,
},
blocking=True,
)
assert error.value.translation_domain == HOMEASSISTANT_DOMAIN
assert error.value.translation_key == "service_device_wrong_domain"
@pytest.mark.parametrize(
("other_entry_state", "translation_domain", "translation_key"),
[
pytest.param(
ConfigEntryState.LOADED,
DOMAIN,
"different_entries",
id="loaded-entry",
),
pytest.param(
ConfigEntryState.NOT_LOADED,
HOMEASSISTANT_DOMAIN,
"service_config_entry_not_loaded",
id="not-loaded-entry",
),
],
)
async def test_devices_from_different_entries(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
transfer_devices: TransferDevices,
other_entry_state: ConfigEntryState,
translation_domain: str,
translation_key: str,
) -> None:
"""Test the account and pot must belong to the same loaded config entry."""
other_entry = MockConfigEntry(domain=DOMAIN)
other_entry.add_to_hass(hass)
other_entry.mock_state(hass, other_entry_state)
other_pot = device_registry.async_get_or_create(
config_entry_id=other_entry.entry_id,
identifiers={(DOMAIN, "other-pot")},
name="Other pot",
)
with pytest.raises(ServiceValidationError):
with pytest.raises(ServiceValidationError) as error:
await hass.services.async_call(
DOMAIN,
SERVICE_DEPOSIT_INTO_POT,
@@ -257,6 +312,9 @@ async def test_devices_from_different_entries(
blocking=True,
)
assert error.value.translation_domain == translation_domain
assert error.value.translation_key == translation_key
async def test_pot_must_belong_to_selected_account(
hass: HomeAssistant,