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

  1. Treat it as usually transient: check the Brunt cloud status and your internet connection; entities typically recover on the next scheduled poll.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of home-assistant/core@58a3fdb3ea (2026-08-14). Data as JSON: /api/errors/dfdfb7d9c478d722. Report an issue: GitHub.