{"record":{"id":"dfdfb7d9c478d722","repo":"home-assistant/core","slug":"unable-find-multi-factor-auth-module-mfa-module","errorCode":null,"errorMessage":"Unable find multi-factor auth module: {mfa_module_id}","messagePattern":"Unable find multi-factor auth module: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"homeassistant/auth/__init__.py","lineNumber":425,"sourceCode":"        \"\"\"Remove credentials.\"\"\"\n        provider = self._async_get_auth_provider(credentials)\n\n        if provider is not None and hasattr(provider, \"async_will_remove_credentials\"):\n            await provider.async_will_remove_credentials(credentials)\n\n        await self._store.async_remove_credentials(credentials)\n\n    async def async_enable_user_mfa(\n        self, user: models.User, mfa_module_id: str, data: Any\n    ) -> None:\n        \"\"\"Enable a multi-factor auth module for user.\"\"\"\n        if user.system_generated:\n            raise ValueError(\n                \"System generated users cannot enable multi-factor auth module.\"\n            )\n\n        if (module := self.get_auth_mfa_module(mfa_module_id)) is None:\n            raise ValueError(f\"Unable find multi-factor auth module: {mfa_module_id}\")\n\n        await module.async_setup_user(user.id, data)\n\n    async def async_disable_user_mfa(\n        self, user: models.User, mfa_module_id: str\n    ) -> None:\n        \"\"\"Disable a multi-factor auth module for user.\"\"\"\n        if user.system_generated:\n            raise ValueError(\n                \"System generated users cannot disable multi-factor auth module.\"\n            )\n\n        if (module := self.get_auth_mfa_module(mfa_module_id)) is None:\n            raise ValueError(f\"Unable find multi-factor auth module: {mfa_module_id}\")\n\n        await module.async_depose_user(user.id)\n\n    async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]:","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/home-assistant/core/blob/58a3fdb3ea0538617f0a07efcfba6294de64fd59/homeassistant/auth/__init__.py#L407-L443","documentation":"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).","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nexcept ClientResponseError as err:\n    if err.status == 403:\n        raise ConfigEntryAuthFailed from err\n    raise UpdateFailed from err\n// after: include the status for diagnosability\nexcept ClientResponseError as err:\n    if err.status == 403:\n        raise ConfigEntryAuthFailed from err\n    raise UpdateFailed(f\"Brunt API error {err.status}: {err.message}\") from err","handlingStrategy":"retry","validationCode":"# Coordinator-level guard: don't hammer a known-flaky endpoint\nfrom datetime import datetime, UTC, timedelta\n\ndef should_poll(now: datetime, last_ok: datetime, min_gap: timedelta = timedelta(seconds=30)) -> bool:\n    return now - last_ok >= min_gap","typeGuard":"from aiohttp import ClientResponseError, ServerDisconnectedError\n\ndef is_transient(err: BaseException) -> bool:\n    \"\"\"True when the failure is worth retrying rather than escalating.\"\"\"\n    return isinstance(err, ServerDisconnectedError) or (\n        isinstance(err, ClientResponseError) and err.status >= 500\n    )","tryCatchPattern":"try:\n    async with timeout(10):\n        things = await self.bapi.async_get_things(force=True)\n        return {t.serial: t for t in things}\nexcept ServerDisconnectedError as err:\n    raise UpdateFailed(f\"Error communicating with API: {err}\") from err\nexcept ClientResponseError as err:\n    if err.status == 403:\n        raise ConfigEntryAuthFailed from err\n    raise UpdateFailed from err  # coordinator retries next interval; entity goes unavailable","preventionTips":["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."],"tags":["home-assistant","coordinator","update-failed","brunt","network","http-error"],"backgroundTag":null,"analyzedSha":"58a3fdb3ea0538617f0a07efcfba6294de64fd59","analyzedAt":"2026-08-14T20:54:38.818Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}