home-assistant/core · error · ValueError
Unable find multi-factor auth module: {mfa_module_id}
Error message
Unable find multi-factor auth module: {mfa_module_id} What it means
UpdateFailed is the DataUpdateCoordinator error meaning 'this polling cycle failed'; the coordinator keeps the entity alive and retries at the next interval, marking entities unavailable rather than aborting setup. brunt raises it when fetching the device list (async_get_things) fails with a ServerDisconnectedError or any ClientResponseError other than 403 (401 is handled as device-deleted plus a reload before re-raising).
Source
Thrown at homeassistant/auth/__init__.py:425
"""Remove credentials."""
provider = self._async_get_auth_provider(credentials)
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
await provider.async_will_remove_credentials(credentials)
await self._store.async_remove_credentials(credentials)
async def async_enable_user_mfa(
self, user: models.User, mfa_module_id: str, data: Any
) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError(
"System generated users cannot enable multi-factor auth module."
)
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(
self, user: models.User, mfa_module_id: str
) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError(
"System generated users cannot disable multi-factor auth module."
)
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_depose_user(user.id)
async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]:View on GitHub (pinned to 58a3fdb3ea)
Solutions
- Treat it as usually transient: check the Brunt cloud status and your internet connection; entities typically recover on the next scheduled poll.
- If the log shows 'Device not found, will reload Brunt integration' (401), remove the deleted device from the Brunt account UI in HA or re-add it in the Brunt app — the integration reloads itself.
- If failures repeat constantly, lower the coordinator's update interval (update_interval in the coordinator) to avoid rate limits, and inspect logs for the underlying status code.
- Verify credentials if UpdateFailed recurs with auth-ish status codes; a 403 would instead surface as ConfigEntryAuthFailed and a re-auth prompt.
Example fix
// before
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed from err
raise UpdateFailed from err
// after: include the status for diagnosability
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed from err
raise UpdateFailed(f"Brunt API error {err.status}: {err.message}") from err Defensive patterns
Strategy: retry
Validate before calling
# Coordinator-level guard: don't hammer a known-flaky endpoint
from datetime import datetime, UTC, timedelta
def should_poll(now: datetime, last_ok: datetime, min_gap: timedelta = timedelta(seconds=30)) -> bool:
return now - last_ok >= min_gap Type guard
from aiohttp import ClientResponseError, ServerDisconnectedError
def is_transient(err: BaseException) -> bool:
"""True when the failure is worth retrying rather than escalating."""
return isinstance(err, ServerDisconnectedError) or (
isinstance(err, ClientResponseError) and err.status >= 500
) Try / catch
try:
async with timeout(10):
things = await self.bapi.async_get_things(force=True)
return {t.serial: t for t in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed from err
raise UpdateFailed from err # coordinator retries next interval; entity goes unavailable Prevention
- Rely on the DataUpdateCoordinator's built-in retry — never crash setup from a poll; UpdateFailed is the correct signal.
- Wrap the request in a timeout (as the source does) so a hung server becomes a clean failure instead of blocking the coordinator.
- Differentiate statuses: 403 -> reauth, 401 -> reload/refresh entities, 5xx -> transient UpdateFailed.
- Set a sane update_interval; aggressive polling invites rate limits that manifest as repeated UpdateFailed.
When it happens
Trigger: self.bapi.async_get_things(force=True) either (a) the server closes the connection mid-request (ServerDisconnectedError — network drop, proxy timeout, Brunt cloud restarting), or (b) returns an HTTP error status (ClientResponseError) such as 401 device-not-in-account (after triggering an integration reload) or 5xx; the bare `raise UpdateFailed from err` at the end of the except chain converts any remaining ClientResponseError.
Common situations: Brunt cloud outage or maintenance, flaky internet/NAT dropping long-lived connections, a device deleted from the Brunt account (401 path triggers a one-time reload then still reports UpdateFailed for that cycle), or rate limiting from too-frequent polling (update_interval too aggressive).
Related errors
- {err}
- Error communicating with API: {err}
- update_error
- Failed to communicate with device.
- Failed to connect
AI-assisted analysis of home-assistant/core@58a3fdb3ea (2026-08-14).
Data as JSON: /api/errors/dfdfb7d9c478d722.
Report an issue: GitHub.